diff --git a/StackExchange.Redis.sln.DotSettings b/StackExchange.Redis.sln.DotSettings
index 8dd9095d9..339a73c59 100644
--- a/StackExchange.Redis.sln.DotSettings
+++ b/StackExchange.Redis.sln.DotSettings
@@ -26,4 +26,5 @@
True
True
True
- True
\ No newline at end of file
+ True
+ True
\ No newline at end of file
diff --git a/docs/exp/SER004.md b/docs/exp/SER004.md
new file mode 100644
index 000000000..91f5d87c4
--- /dev/null
+++ b/docs/exp/SER004.md
@@ -0,0 +1,15 @@
+# RESPite
+
+RESPite is an experimental library that provides high-performance low-level RESP (Redis, etc) parsing and serialization.
+It is used as the IO core for StackExchange.Redis v3+. You should not (yet) use it directly unless you have a very
+good reason to do so.
+
+```xml
+$(NoWarn);SER004
+```
+
+or more granularly / locally in C#:
+
+``` c#
+#pragma warning disable SER004
+```
diff --git a/docs/exp/SER005.md b/docs/exp/SER005.md
new file mode 100644
index 000000000..03e7b7bb4
--- /dev/null
+++ b/docs/exp/SER005.md
@@ -0,0 +1,21 @@
+# Unit Testing
+
+Unit testing is great! Yay, do more of that!
+
+This type is provided for external unit testing, in particular by people using modules or server features
+not directly implemented by SE.Redis - for example to verify messsage parsing or formatting without
+talking to a RESP server.
+
+These types are considered slightly more... *mercurial*. We encourage you to use them, but *occasionally*
+(not just for fun) you might need to update your test code if we tweak something. This should not impact
+"real" library usage.
+
+```xml
+$(NoWarn);SER005
+```
+
+or more granularly / locally in C#:
+
+``` c#
+#pragma warning disable SER005
+```
diff --git a/src/RESPite/PublicAPI/net6.0/PublicAPI.Shipped.txt b/src/RESPite/PublicAPI/net6.0/PublicAPI.Shipped.txt
new file mode 100644
index 000000000..ab058de62
--- /dev/null
+++ b/src/RESPite/PublicAPI/net6.0/PublicAPI.Shipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/RESPite/PublicAPI/net6.0/PublicAPI.Unshipped.txt b/src/RESPite/PublicAPI/net6.0/PublicAPI.Unshipped.txt
new file mode 100644
index 000000000..ab058de62
--- /dev/null
+++ b/src/RESPite/PublicAPI/net6.0/PublicAPI.Unshipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/RESPite/Shared/AsciiHash.Public.cs b/src/RESPite/Shared/AsciiHash.Public.cs
deleted file mode 100644
index dd31cb415..000000000
--- a/src/RESPite/Shared/AsciiHash.Public.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-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
index 37b3c5734..2a995425d 100644
--- a/src/RESPite/Shared/AsciiHash.cs
+++ b/src/RESPite/Shared/AsciiHash.cs
@@ -1,4 +1,3 @@
-using System;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
@@ -7,8 +6,6 @@
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
@@ -22,7 +19,7 @@ namespace RESPite;
Inherited = false)]
[Conditional("DEBUG")] // evaporate in release
[Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)]
-sealed partial class AsciiHashAttribute(string token = "") : Attribute
+public sealed partial class AsciiHashAttribute(string token = "") : Attribute
{
///
/// The token expected when parsing data, if different from the implied value. The implied
@@ -38,7 +35,7 @@ sealed partial class AsciiHashAttribute(string token = "") : Attribute
// note: instance members are in AsciiHash.Instance.cs.
[Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)]
-readonly partial struct AsciiHash
+public readonly partial struct AsciiHash
{
///
/// In-place ASCII upper-case conversion.
diff --git a/src/StackExchange.Redis/APITypes/LatencyHistoryEntry.cs b/src/StackExchange.Redis/APITypes/LatencyHistoryEntry.cs
index 003708e6a..0daa00377 100644
--- a/src/StackExchange.Redis/APITypes/LatencyHistoryEntry.cs
+++ b/src/StackExchange.Redis/APITypes/LatencyHistoryEntry.cs
@@ -1,4 +1,5 @@
using System;
+using RESPite.Messages;
namespace StackExchange.Redis;
@@ -11,18 +12,14 @@ public readonly struct LatencyHistoryEntry
private sealed class Processor : ArrayResultProcessor
{
- protected override bool TryParse(in RawResult raw, out LatencyHistoryEntry parsed)
+ protected override bool TryParse(ref RespReader reader, out LatencyHistoryEntry parsed)
{
- if (raw.Resp2TypeArray == ResultType.Array)
+ if (reader.IsAggregate
+ && reader.TryMoveNext() && reader.IsScalar && reader.TryReadInt64(out var timestamp)
+ && reader.TryMoveNext() && reader.IsScalar && reader.TryReadInt64(out var duration))
{
- 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 = new LatencyHistoryEntry(timestamp, duration);
+ return true;
}
parsed = default;
return false;
diff --git a/src/StackExchange.Redis/APITypes/LatencyLatestEntry.cs b/src/StackExchange.Redis/APITypes/LatencyLatestEntry.cs
index d1bc70e42..c1f012495 100644
--- a/src/StackExchange.Redis/APITypes/LatencyLatestEntry.cs
+++ b/src/StackExchange.Redis/APITypes/LatencyLatestEntry.cs
@@ -1,4 +1,5 @@
using System;
+using RESPite.Messages;
namespace StackExchange.Redis;
@@ -11,17 +12,17 @@ public readonly struct LatencyLatestEntry
private sealed class Processor : ArrayResultProcessor
{
- protected override bool TryParse(in RawResult raw, out LatencyLatestEntry parsed)
+ protected override bool TryParse(ref RespReader reader, out LatencyLatestEntry parsed)
{
- if (raw.Resp2TypeArray == ResultType.Array)
+ if (reader.IsAggregate && reader.TryMoveNext() && reader.IsScalar)
{
- 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))
+ var eventName = reader.ReadString()!;
+
+ if (reader.TryMoveNext() && reader.IsScalar && reader.TryReadInt64(out var timestamp)
+ && reader.TryMoveNext() && reader.IsScalar && reader.TryReadInt64(out var duration)
+ && reader.TryMoveNext() && reader.IsScalar && reader.TryReadInt64(out var maxDuration))
{
- parsed = new LatencyLatestEntry(items[0].GetString()!, timestamp, duration, maxDuration);
+ parsed = new LatencyLatestEntry(eventName, timestamp, duration, maxDuration);
return true;
}
}
diff --git a/src/StackExchange.Redis/BufferedStreamWriter.Async.cs b/src/StackExchange.Redis/BufferedStreamWriter.Async.cs
new file mode 100644
index 000000000..3c2cebee7
--- /dev/null
+++ b/src/StackExchange.Redis/BufferedStreamWriter.Async.cs
@@ -0,0 +1,132 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Threading.Tasks.Sources;
+
+namespace StackExchange.Redis;
+
+internal sealed class BufferedAsyncStreamWriter : CycleBufferStreamWriter, IValueTaskSource
+{
+ private ManualResetValueTaskSourceCore _readerTask;
+
+ public BufferedAsyncStreamWriter(Stream target, CancellationToken cancellationToken = default)
+ : base(target, cancellationToken)
+ {
+ WriteComplete = Task.Run(CopyOutAsync, cancellationToken);
+ _readerTask.RunContinuationsAsynchronously = true; // we never want the flusher to take over the copying
+ }
+
+ public override Task WriteComplete { get; }
+
+ private async Task CopyOutAsync()
+ {
+ try
+ {
+ while (true)
+ {
+ ValueTask pending = AwaitWake();
+ if (!pending.IsCompleted)
+ {
+ lock (this)
+ {
+ // double-checked marking inactive
+ if (!pending.IsCompleted) OnWriterInactive(); // update state flags
+ }
+ }
+ // await activation and check status;
+ await pending.ConfigureAwait(false);
+
+ StateFlags stateFlags;
+ while (true)
+ {
+ ReadOnlyMemory memory;
+ lock (this)
+ {
+ stateFlags = State;
+ var minBytes = (stateFlags & StateFlags.Flush) == 0 ? -1 : 1;
+ if (!GetFirstChunkInsideLock(minBytes, out memory))
+ {
+ // out of data; remove flush flag and wait for more work
+ stateFlags &= ~StateFlags.Flush;
+ break;
+ }
+ }
+
+ if (IsFaulted) ThrowCompleteOrFaulted(); // this is cheap to check ongoing
+ if (!memory.IsEmpty)
+ {
+ OnWritten(memory.Length);
+ OnDebugBufferLog(memory);
+
+ await Target.WriteAsync(memory, CancellationToken).ConfigureAwait(false);
+ }
+
+ lock (this)
+ {
+ DiscardCommitted(memory.Length);
+ }
+ }
+ await Target.FlushAsync(CancellationToken).ConfigureAwait(false);
+
+ if ((stateFlags & StateFlags.Closed) != 0) break;
+ }
+
+ // recycle on clean exit (only), since we know the buffers aren't being used
+ lock (this)
+ {
+ ReleaseBuffer();
+ }
+ }
+ catch (Exception ex)
+ {
+ Complete(ex);
+ }
+ // note we do *not* close the stream here - we have to settle for flushing; Close is explicit
+ }
+
+ private ValueTask AwaitWake()
+ {
+ lock (this) // guard all transitions
+ {
+ return new(this, _readerTask.Version);
+ }
+ }
+
+ void IValueTaskSource.GetResult(short token)
+ {
+ lock (this) // guard all transitions
+ {
+ _readerTask.GetResult(token); // may throw, note
+ _readerTask.Reset();
+ }
+ }
+
+ ValueTaskSourceStatus IValueTaskSource.GetStatus(short token)
+ {
+ lock (this) // guard all transitions
+ {
+ return _readerTask.GetStatus(token);
+ }
+ }
+
+ void IValueTaskSource.OnCompleted(
+ Action
public sealed class CommandMap
{
- private readonly CommandBytes[] map;
+ private readonly AsciiHash[] map;
- internal CommandMap(CommandBytes[] map) => this.map = map;
+ internal CommandMap(AsciiHash[] map) => this.map = map;
///
/// The default commands specified by redis.
@@ -132,7 +133,7 @@ public static CommandMap Create(HashSet commands, bool available = true)
{
var dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase);
// nix everything
- foreach (RedisCommand command in (RedisCommand[])Enum.GetValues(typeof(RedisCommand)))
+ foreach (RedisCommand command in AllCommands)
{
dictionary[command.ToString()] = null;
}
@@ -154,7 +155,7 @@ public static CommandMap Create(HashSet commands, bool available = true)
// nix the things that are specified
foreach (var command in commands)
{
- if (Enum.TryParse(command, true, out RedisCommand parsed))
+ if (RedisCommandMetadata.TryParseCI(command, out RedisCommand parsed))
{
(exclusions ??= new HashSet()).Add(parsed);
}
@@ -177,10 +178,13 @@ public override string ToString()
internal void AppendDeltas(StringBuilder sb)
{
+ var all = AllCommands;
for (int i = 0; i < map.Length; i++)
{
- var keyString = ((RedisCommand)i).ToString();
- var keyBytes = new CommandBytes(keyString);
+ var knownCmd = all[i];
+ if (knownCmd is RedisCommand.UNKNOWN) continue;
+ var keyString = knownCmd.ToString();
+ var keyBytes = new AsciiHash(keyString);
var value = map[i];
if (!keyBytes.Equals(value))
{
@@ -195,32 +199,26 @@ internal void AssertAvailable(RedisCommand command)
if (map[(int)command].IsEmpty) throw ExceptionFactory.CommandDisabled(command);
}
- internal CommandBytes GetBytes(RedisCommand command) => map[(int)command];
-
- internal CommandBytes GetBytes(string command)
- {
- if (command == null) return default;
- if (Enum.TryParse(command, true, out RedisCommand cmd))
- {
- // we know that one!
- return map[(int)cmd];
- }
- return new CommandBytes(command);
- }
+ internal AsciiHash GetBytes(RedisCommand command) => map[(int)command];
internal bool IsAvailable(RedisCommand command) => !map[(int)command].IsEmpty;
+ private static RedisCommand[]? s_AllCommands;
+
+ private static ReadOnlySpan AllCommands => s_AllCommands ??= (RedisCommand[])Enum.GetValues(typeof(RedisCommand));
+
private static CommandMap CreateImpl(Dictionary? caseInsensitiveOverrides, HashSet? exclusions)
{
- var commands = (RedisCommand[])Enum.GetValues(typeof(RedisCommand));
+ var commands = AllCommands;
- var map = new CommandBytes[commands.Length];
+ // todo: optimize and support ad-hoc overrides/disables, and shared buffer rather than multiple arrays
+ var map = new AsciiHash[commands.Length];
for (int i = 0; i < commands.Length; i++)
{
int idx = (int)commands[i];
string? name = commands[i].ToString(), value = name;
- if (exclusions?.Contains(commands[i]) == true)
+ if (commands[i] is RedisCommand.UNKNOWN || exclusions?.Contains(commands[i]) == true)
{
map[idx] = default;
}
@@ -228,9 +226,9 @@ private static CommandMap CreateImpl(Dictionary? caseInsensitiv
{
if (caseInsensitiveOverrides != null && caseInsensitiveOverrides.TryGetValue(name, out string? tmp))
{
- value = tmp;
+ value = tmp?.ToUpperInvariant();
}
- map[idx] = new CommandBytes(value);
+ map[idx] = new AsciiHash(value);
}
}
return new CommandMap(map);
diff --git a/src/StackExchange.Redis/CommandTrace.cs b/src/StackExchange.Redis/CommandTrace.cs
index a61499f0c..7125bc7b3 100644
--- a/src/StackExchange.Redis/CommandTrace.cs
+++ b/src/StackExchange.Redis/CommandTrace.cs
@@ -1,4 +1,5 @@
using System;
+using RESPite.Messages;
namespace StackExchange.Redis
{
@@ -71,25 +72,37 @@ internal CommandTrace(long uniqueId, long time, long duration, RedisValue[] argu
private sealed class CommandTraceProcessor : ResultProcessor
{
- protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result)
+ protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader)
{
- switch (result.Resp2TypeArray)
+ // see: SLOWLOG GET
+ if (reader.IsAggregate)
{
- case ResultType.Array:
- var parts = result.GetItems();
- CommandTrace[] arr = new CommandTrace[parts.Length];
- int i = 0;
- foreach (var item in parts)
+ var arr = reader.ReadPastArray(ParseOne, scalar: false)!;
+ if (arr.AnyNull()) return false;
+
+ SetResult(message, arr);
+ return true;
+ }
+
+ return false;
+
+ static CommandTrace ParseOne(ref RespReader reader)
+ {
+ CommandTrace result = null!;
+ if (reader.IsAggregate)
+ {
+ long uniqueId = 0, time = 0, duration = 0;
+ if (reader.TryMoveNext() && reader.IsScalar && reader.TryReadInt64(out uniqueId)
+ && reader.TryMoveNext() && reader.IsScalar && reader.TryReadInt64(out time)
+ && reader.TryMoveNext() && reader.IsScalar && reader.TryReadInt64(out duration)
+ && reader.TryMoveNext() && reader.IsAggregate)
{
- 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()!);
+ var values = reader.ReadPastRedisValues() ?? [];
+ result = new CommandTrace(uniqueId, time, duration, values);
}
- SetResult(message, arr);
- return true;
+ }
+ return result;
}
- return false;
}
}
}
diff --git a/src/StackExchange.Redis/Condition.cs b/src/StackExchange.Redis/Condition.cs
index ec7ee53b6..fa7e76299 100644
--- a/src/StackExchange.Redis/Condition.cs
+++ b/src/StackExchange.Redis/Condition.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using RESPite.Messages;
namespace StackExchange.Redis
{
@@ -372,7 +373,8 @@ public static Condition StringNotEqual(RedisKey key, RedisValue value)
internal abstract IEnumerable CreateMessages(int db, IResultBox? resultBox);
internal abstract int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy);
- internal abstract bool TryValidate(in RawResult result, out bool value);
+
+ internal abstract bool TryValidate(ref RespReader reader, out bool value);
internal sealed class ConditionProcessor : ResultProcessor
{
@@ -387,13 +389,12 @@ 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, 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)
+ protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader)
{
- connection?.BridgeCouldBeNull?.Multiplexer?.OnTransactionLog($"condition '{message.CommandAndKey}' got '{result.ToString()}'");
+ connection?.BridgeCouldBeNull?.Multiplexer?.OnTransactionLog($"condition '{message.CommandAndKey}' got '{reader.GetOverview()}'");
var msg = message as ConditionMessage;
var condition = msg?.Condition;
- if (condition != null && condition.TryValidate(result, out bool final))
+ if (condition != null && condition.TryValidate(ref reader, out bool final))
{
SetResult(message, final);
return true;
@@ -432,26 +433,26 @@ public ConditionMessage(Condition condition, int db, CommandFlags flags, RedisCo
this.value4 = value4; // note no assert here
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
if (value.IsNull)
{
- physical.WriteHeader(command, 1);
- physical.Write(Key);
+ writer.WriteHeader(command, 1);
+ writer.Write(Key);
}
else
{
- physical.WriteHeader(command, value1.IsNull ? 2 : value2.IsNull ? 3 : value3.IsNull ? 4 : value4.IsNull ? 5 : 6);
- physical.Write(Key);
- physical.WriteBulkString(value);
+ writer.WriteHeader(command, value1.IsNull ? 2 : value2.IsNull ? 3 : value3.IsNull ? 4 : value4.IsNull ? 5 : 6);
+ writer.Write(Key);
+ writer.WriteBulkString(value);
if (!value1.IsNull)
- physical.WriteBulkString(value1);
+ writer.WriteBulkString(value1);
if (!value2.IsNull)
- physical.WriteBulkString(value2);
+ writer.WriteBulkString(value2);
if (!value3.IsNull)
- physical.WriteBulkString(value3);
+ writer.WriteBulkString(value3);
if (!value4.IsNull)
- physical.WriteBulkString(value4);
+ writer.WriteBulkString(value4);
}
}
public override int ArgCount => value.IsNull ? 1 : value1.IsNull ? 2 : value2.IsNull ? 3 : value3.IsNull ? 4 : value4.IsNull ? 5 : 6;
@@ -510,19 +511,20 @@ internal override IEnumerable CreateMessages(int db, IResultBox? result
internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(key);
- internal override bool TryValidate(in RawResult result, out bool value)
+ internal override bool TryValidate(ref RespReader reader, out bool value)
{
switch (type)
{
case RedisType.SortedSet:
- var parsedValue = result.AsRedisValue();
- value = parsedValue.IsNull != expectedResult;
- ConnectionMultiplexer.TraceWithoutContext("exists: " + parsedValue + "; expected: " + expectedResult + "; voting: " + value);
+ // ZSCORE returns bulk string (score) or null
+ var parsedValue = reader.IsNull;
+ value = parsedValue != expectedResult;
+ ConnectionMultiplexer.TraceWithoutContext("exists: " + !parsedValue + "; expected: " + expectedResult + "; voting: " + value);
return true;
default:
- bool parsed;
- if (ResultProcessor.DemandZeroOrOneProcessor.TryGet(result, out parsed))
+ // EXISTS, HEXISTS, SISMEMBER return integer 0 or 1
+ if (ResultProcessor.DemandZeroOrOneProcessor.TryGet(ref reader, out bool parsed))
{
value = parsed == expectedResult;
ConnectionMultiplexer.TraceWithoutContext("exists: " + parsed + "; expected: " + expectedResult + "; voting: " + value);
@@ -586,12 +588,30 @@ internal override IEnumerable CreateMessages(int db, IResultBox? result
internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(key);
- internal override bool TryValidate(in RawResult result, out bool value)
+ internal override bool TryValidate(ref RespReader reader, out bool value)
{
- value = result.ItemsCount == 1 && result[0].AsRedisValue().StartsWith(prefix);
-
- if (!expectedResult) value = !value;
- return true;
+ // ZRANGEBYLEX returns an array with 0 or 1 elements
+ if (reader.IsAggregate)
+ {
+ var count = reader.AggregateLength();
+ if (count == 1)
+ {
+ // Check if the first element starts with prefix
+ if (reader.TryMoveNext() && reader.IsScalar)
+ {
+ value = reader.ReadRedisValue().StartsWith(prefix);
+ if (!expectedResult) value = !value;
+ return true;
+ }
+ }
+ else if (count == 0)
+ {
+ value = !expectedResult; // No match found
+ return true;
+ }
+ }
+ value = false;
+ return false;
}
}
@@ -640,13 +660,21 @@ internal override IEnumerable CreateMessages(int db, IResultBox? result
internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(key);
- internal override bool TryValidate(in RawResult result, out bool value)
+ internal override bool TryValidate(ref RespReader reader, out bool value)
{
+ // All commands (ZSCORE, GET, HGET) return scalar values
+ if (!reader.IsScalar)
+ {
+ value = false;
+ return false;
+ }
+
switch (type)
{
case RedisType.SortedSet:
+ // ZSCORE returns bulk string (score as double) or null
var parsedValue = RedisValue.Null;
- if (!result.IsNull && result.TryGetDouble(out var val))
+ if (!reader.IsNull && reader.TryReadDouble(out var val))
{
parsedValue = val;
}
@@ -657,19 +685,12 @@ internal override bool TryValidate(in RawResult result, out bool value)
return true;
default:
- switch (result.Resp2TypeBulkString)
- {
- case ResultType.BulkString:
- case ResultType.SimpleString:
- case ResultType.Integer:
- var parsed = result.AsRedisValue();
- value = (parsed == expectedValue) == expectedEqual;
- ConnectionMultiplexer.TraceWithoutContext("actual: " + (string?)parsed + "; expected: " + (string?)expectedValue +
- "; wanted: " + (expectedEqual ? "==" : "!=") + "; voting: " + value);
- return true;
- }
- value = false;
- return false;
+ // GET or HGET returns bulk string, simple string, or integer
+ var parsed = reader.ReadRedisValue();
+ value = (parsed == expectedValue) == expectedEqual;
+ ConnectionMultiplexer.TraceWithoutContext("actual: " + (string?)parsed + "; expected: " + (string?)expectedValue +
+ "; wanted: " + (expectedEqual ? "==" : "!=") + "; voting: " + value);
+ return true;
}
}
}
@@ -711,26 +732,24 @@ internal override IEnumerable CreateMessages(int db, IResultBox? result
internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(key);
- internal override bool TryValidate(in RawResult result, out bool value)
+ internal override bool TryValidate(ref RespReader reader, out bool value)
{
- switch (result.Resp2TypeBulkString)
+ // LINDEX returns bulk string, simple string, or integer
+ if (reader.IsScalar)
{
- case ResultType.BulkString:
- case ResultType.SimpleString:
- case ResultType.Integer:
- var parsed = result.AsRedisValue();
- if (expectedValue.HasValue)
- {
- value = (parsed == expectedValue.Value) == expectedResult;
- ConnectionMultiplexer.TraceWithoutContext("actual: " + (string?)parsed + "; expected: " + (string?)expectedValue.Value +
- "; wanted: " + (expectedResult ? "==" : "!=") + "; voting: " + value);
- }
- else
- {
- value = parsed.IsNull != expectedResult;
- ConnectionMultiplexer.TraceWithoutContext("exists: " + parsed + "; expected: " + expectedResult + "; voting: " + value);
- }
- return true;
+ var parsed = reader.ReadRedisValue();
+ if (expectedValue.HasValue)
+ {
+ value = (parsed == expectedValue.Value) == expectedResult;
+ ConnectionMultiplexer.TraceWithoutContext("actual: " + (string?)parsed + "; expected: " + (string?)expectedValue.Value +
+ "; wanted: " + (expectedResult ? "==" : "!=") + "; voting: " + value);
+ }
+ else
+ {
+ value = parsed.IsNull != expectedResult;
+ ConnectionMultiplexer.TraceWithoutContext("exists: " + parsed + "; expected: " + expectedResult + "; voting: " + value);
+ }
+ return true;
}
value = false;
return false;
@@ -784,18 +803,15 @@ internal override IEnumerable CreateMessages(int db, IResultBox? result
internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(key);
- internal override bool TryValidate(in RawResult result, out bool value)
+ internal override bool TryValidate(ref RespReader reader, out bool value)
{
- switch (result.Resp2TypeBulkString)
+ // Length commands (HLEN, SCARD, LLEN, ZCARD, XLEN, STRLEN) return integer
+ if (reader.IsScalar && reader.TryReadInt64(out var parsed))
{
- case ResultType.BulkString:
- case ResultType.SimpleString:
- case ResultType.Integer:
- var parsed = result.AsRedisValue();
- value = parsed.IsInteger && (expectedLength.CompareTo((long)parsed) == compareToResult);
- ConnectionMultiplexer.TraceWithoutContext("actual: " + (string?)parsed + "; expected: " + expectedLength +
- "; wanted: " + GetComparisonString() + "; voting: " + value);
- return true;
+ value = expectedLength.CompareTo(parsed) == compareToResult;
+ ConnectionMultiplexer.TraceWithoutContext("actual: " + parsed + "; expected: " + expectedLength +
+ "; wanted: " + GetComparisonString() + "; voting: " + value);
+ return true;
}
value = false;
return false;
@@ -841,18 +857,16 @@ internal override IEnumerable CreateMessages(int db, IResultBox? result
internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(key);
- internal override bool TryValidate(in RawResult result, out bool value)
+ internal override bool TryValidate(ref RespReader reader, out bool value)
{
- switch (result.Resp2TypeBulkString)
+ // Length commands (HLEN, SCARD, LLEN, ZCARD, XLEN, STRLEN) return integer
+ if (reader.IsScalar)
{
- case ResultType.BulkString:
- case ResultType.SimpleString:
- case ResultType.Integer:
- var parsed = result.AsRedisValue();
- value = parsed.IsInteger && (expectedLength.CompareTo((long)parsed) == compareToResult);
- ConnectionMultiplexer.TraceWithoutContext("actual: " + (string?)parsed + "; expected: " + expectedLength +
- "; wanted: " + GetComparisonString() + "; voting: " + value);
- return true;
+ var parsed = reader.ReadRedisValue();
+ value = parsed.IsInteger && (expectedLength.CompareTo((long)parsed) == compareToResult);
+ ConnectionMultiplexer.TraceWithoutContext("actual: " + (string?)parsed + "; expected: " + expectedLength +
+ "; wanted: " + GetComparisonString() + "; voting: " + value);
+ return true;
}
value = false;
return false;
@@ -898,17 +912,16 @@ internal override IEnumerable CreateMessages(int db, IResultBox? result
internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(key);
- internal override bool TryValidate(in RawResult result, out bool value)
+ internal override bool TryValidate(ref RespReader reader, out bool value)
{
- switch (result.Resp2TypeBulkString)
+ // ZCOUNT returns integer
+ if (reader.IsScalar)
{
- case ResultType.Integer:
- var parsedValue = result.AsRedisValue();
- value = (parsedValue == expectedValue) == expectedEqual;
- ConnectionMultiplexer.TraceWithoutContext("actual: " + (string?)parsedValue + "; expected: " + (string?)expectedValue + "; wanted: " + (expectedEqual ? "==" : "!=") + "; voting: " + value);
- return true;
+ var parsedValue = reader.ReadRedisValue();
+ value = (parsedValue == expectedValue) == expectedEqual;
+ ConnectionMultiplexer.TraceWithoutContext("actual: " + (string?)parsedValue + "; expected: " + (string?)expectedValue + "; wanted: " + (expectedEqual ? "==" : "!=") + "; voting: " + value);
+ return true;
}
-
value = false;
return false;
}
diff --git a/src/StackExchange.Redis/ConfigField.cs b/src/StackExchange.Redis/ConfigField.cs
new file mode 100644
index 000000000..b39e1e029
--- /dev/null
+++ b/src/StackExchange.Redis/ConfigField.cs
@@ -0,0 +1,49 @@
+using System;
+using RESPite;
+
+namespace StackExchange.Redis;
+
+///
+/// Fields that can appear in a CONFIG GET command response.
+///
+internal enum ConfigField
+{
+ ///
+ /// Unknown or unrecognized field.
+ ///
+ [AsciiHash("")]
+ Unknown = 0,
+
+ ///
+ /// Timeout configuration.
+ ///
+ [AsciiHash("timeout")]
+ Timeout,
+
+ ///
+ /// Number of databases.
+ ///
+ [AsciiHash("databases")]
+ Databases,
+
+ ///
+ /// Replica read-only setting (slave-read-only).
+ ///
+ [AsciiHash("slave-read-only")]
+ SlaveReadOnly,
+
+ ///
+ /// Replica read-only setting (replica-read-only).
+ ///
+ [AsciiHash("replica-read-only")]
+ ReplicaReadOnly,
+}
+
+///
+/// Metadata and parsing methods for ConfigField.
+///
+internal static partial class ConfigFieldMetadata
+{
+ [AsciiHash]
+ internal static partial bool TryParse(ReadOnlySpan value, out ConfigField field);
+}
diff --git a/src/StackExchange.Redis/Configuration/LoggingTunnel.cs b/src/StackExchange.Redis/Configuration/LoggingTunnel.cs
index 18216c1f2..33b6f182b 100644
--- a/src/StackExchange.Redis/Configuration/LoggingTunnel.cs
+++ b/src/StackExchange.Redis/Configuration/LoggingTunnel.cs
@@ -3,15 +3,15 @@
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 Pipelines.Sockets.Unofficial;
-using Pipelines.Sockets.Unofficial.Arenas;
+using RESPite;
+using RESPite.Buffers;
+using RESPite.Messages;
using static StackExchange.Redis.PhysicalConnection;
namespace StackExchange.Redis.Configuration;
@@ -27,25 +27,158 @@ public abstract class LoggingTunnel : Tunnel
private readonly bool _ssl;
private readonly Tunnel? _tail;
+ internal sealed class StreamRespReader(Stream source, bool isInbound) : IDisposable
+ {
+ private CycleBuffer _readBuffer = CycleBuffer.Create();
+ private RespScanState _state;
+ private bool _reading, _disposed; // we need to track the state of the reader to avoid releasing the buffer while it's in use
+
+ public long Position { get; private set; }
+ internal bool TryTakeOne(out ContextualRedisResult result, bool withData = true)
+ {
+ var fullBuffer = _readBuffer.GetAllCommitted();
+ var newData = fullBuffer.Slice(_state.TotalBytes);
+ var status = RespFrameScanner.Default.TryRead(ref _state, newData);
+ switch (status)
+ {
+ case OperationStatus.Done:
+ var frame = fullBuffer.Slice(0, _state.TotalBytes);
+ var reader = new RespReader(frame);
+ reader.MovePastBof();
+ bool isOutOfBand = reader.Prefix is RespPrefix.Push
+ || (isInbound && reader.IsAggregate &&
+ !IsArrayOutOfBand(in reader));
+
+ RedisResult? parsed;
+ if (withData)
+ {
+ if (!RedisResult.TryCreate(null, ref reader, out parsed))
+ {
+ ThrowInvalidReadStatus(OperationStatus.InvalidData);
+ }
+ }
+ else
+ {
+ parsed = null;
+ }
+ result = new(parsed, isOutOfBand);
+ Position += _state.TotalBytes;
+ _readBuffer.DiscardCommitted((int)_state.TotalBytes);
+ _state = default;
+ return true;
+ case OperationStatus.NeedMoreData:
+ result = default;
+ return false;
+ default:
+ ThrowInvalidReadStatus(status);
+ goto case OperationStatus.NeedMoreData; // never reached
+ }
+ }
+
+ private static bool IsArrayOutOfBand(in RespReader source)
+ {
+ var reader = source;
+ int len;
+ if (!reader.IsStreaming
+ && (len = reader.AggregateLength()) >= 2
+ && (reader.SafeTryMoveNext() & reader.IsInlineScalar & !reader.IsError))
+ {
+ const int MAX_TYPE_LEN = 16;
+ var span = reader.TryGetSpan(out var tmp)
+ ? tmp
+ : StackCopyLengthChecked(in reader, stackalloc byte[MAX_TYPE_LEN]);
+
+ if (PushKindMetadata.TryParse(span, out var kind))
+ {
+ return kind switch
+ {
+ PushKind.Message => len >= 3,
+ PushKind.PMessage => len >= 4,
+ PushKind.SMessage => len >= 3,
+ _ => false,
+ };
+ }
+ }
+
+ return false;
+ }
+
+ public ValueTask ReadOneAsync(CancellationToken cancellationToken = default)
+ => TryTakeOne(out var result) ? new(result) : ReadMoreAsync(cancellationToken);
+
+ [DoesNotReturn]
+ private static void ThrowInvalidReadStatus(OperationStatus status)
+ => throw new InvalidOperationException($"Unexpected read status: {status}");
+
+ private async ValueTask ReadMoreAsync(CancellationToken cancellationToken)
+ {
+ while (true)
+ {
+ var buffer = _readBuffer.GetUncommittedMemory();
+ Debug.Assert(!buffer.IsEmpty, "rule out zero-length reads");
+ _reading = true;
+ var read = await source.ReadAsync(buffer, cancellationToken).ForAwait();
+ _reading = false;
+ if (read <= 0)
+ {
+ // EOF
+ return default;
+ }
+ _readBuffer.Commit(read);
+
+ if (TryTakeOne(out var result)) return result;
+ }
+ }
+
+ public void Dispose()
+ {
+ bool disposed = _disposed;
+ _disposed = true;
+ _state = default;
+
+ if (!(_reading | disposed)) _readBuffer.Release();
+ _readBuffer = default;
+ if (!disposed) source.Dispose();
+ }
+
+ public async ValueTask ValidateAsync(CancellationToken cancellationToken = default)
+ {
+ long count = 0;
+ while (true)
+ {
+ var buffer = _readBuffer.GetUncommittedMemory();
+ Debug.Assert(!buffer.IsEmpty, "rule out zero-length reads");
+ _reading = true;
+ var read = await source.ReadAsync(buffer, cancellationToken).ForAwait();
+ _reading = false;
+ if (read <= 0)
+ {
+ // EOF
+ return count;
+ }
+ _readBuffer.Commit(read);
+ while (TryTakeOne(out _, withData: false)) count++;
+ }
+ }
+ }
+
///
/// 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;
+ using var outReader = new StreamRespReader(@out, isInbound: false);
+ using var inReader = new StreamRespReader(@in, isInbound: true);
while (true)
{
- var sent = await ReadOneAsync(outPipe, arena, isInbound: false).ForAwait();
+ if (!outReader.TryTakeOne(out var sent)) sent = await outReader.ReadOneAsync().ForAwait();
ContextualRedisResult received;
try
{
do
{
- received = await ReadOneAsync(inPipe, arena, isInbound: true).ForAwait();
+ if (!inReader.TryTakeOne(out received)) received = await inReader.ReadOneAsync().ForAwait();
if (received.IsOutOfBand && received.Result is not null)
{
// spoof an empty request for OOB messages
@@ -93,26 +226,6 @@ public static async Task ReplayAsync(string path, Action 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.
///
@@ -152,63 +265,11 @@ public static async Task ValidateAsync(string path)
///
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();
- }
- }
+ using var reader = new StreamRespReader(stream, isInbound: false);
+ return await reader.ValidateAsync();
}
- private readonly struct ContextualRedisResult
+ internal readonly struct ContextualRedisResult
{
public readonly RedisResult? Result;
public readonly bool IsOutOfBand;
@@ -219,42 +280,6 @@ public ContextualRedisResult(RedisResult? result, bool 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 .
///
diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs
index 641fccc95..1a0cdd1a6 100644
--- a/src/StackExchange.Redis/ConfigurationOptions.cs
+++ b/src/StackExchange.Redis/ConfigurationOptions.cs
@@ -330,9 +330,9 @@ internal static LocalCertificateSelectionCallback CreatePemUserCertificateCallba
{
// 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
+#pragma warning disable SYSLIB0057 // X509 loading
var pfx = new X509Certificate2(pem.Export(X509ContentType.Pfx));
-#pragma warning restore SYSLIB0057 // Type or member is obsolete
+#pragma warning restore SYSLIB0057 // X509 loading
return (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => pfx;
}
@@ -340,9 +340,9 @@ internal static LocalCertificateSelectionCallback CreatePemUserCertificateCallba
internal static LocalCertificateSelectionCallback CreatePfxUserCertificateCallback(string userCertificatePath, string? password, X509KeyStorageFlags storageFlags = X509KeyStorageFlags.DefaultKeySet)
{
-#pragma warning disable SYSLIB0057
+#pragma warning disable SYSLIB0057 // X509 loading
var pfx = new X509Certificate2(userCertificatePath, password ?? "", storageFlags);
-#pragma warning restore SYSLIB0057
+#pragma warning restore SYSLIB0057 // X509 loading
return (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => pfx;
}
@@ -353,9 +353,10 @@ internal static LocalCertificateSelectionCallback CreatePfxUserCertificateCallba
public void TrustIssuer(X509Certificate2 issuer) => CertificateValidationCallback = TrustIssuerCallback(issuer);
internal static RemoteCertificateValidationCallback TrustIssuerCallback(string issuerCertificatePath)
-#pragma warning disable SYSLIB0057
+#pragma warning disable SYSLIB0057 // X509 loading
=> TrustIssuerCallback(new X509Certificate2(issuerCertificatePath));
-#pragma warning restore SYSLIB0057
+#pragma warning restore SYSLIB0057 // X509 loading
+
private static RemoteCertificateValidationCallback TrustIssuerCallback(X509Certificate2 issuer)
{
if (issuer == null) throw new ArgumentNullException(nameof(issuer));
@@ -696,6 +697,7 @@ public int ResponseTimeout
/// This is only used when a is created.
/// Modifying it afterwards will have no effect on already-created multiplexers.
///
+ [Obsolete("SocketManager is no longer used by StackExchange.Redis")]
public SocketManager? SocketManager { get; set; }
#if NET
@@ -825,7 +827,9 @@ public static ConfigurationOptions Parse(string configuration, bool ignoreUnknow
CertificateValidationCallback = CertificateValidationCallback,
CertificateSelectionCallback = CertificateSelectionCallback,
ChannelPrefix = ChannelPrefix.Clone(),
+#pragma warning disable CS0618 // Type or member is obsolete
SocketManager = SocketManager,
+#pragma warning restore CS0618 // Type or member is obsolete
connectRetry = connectRetry,
configCheckSeconds = configCheckSeconds,
responseTimeout = responseTimeout,
@@ -847,6 +851,10 @@ public static ConfigurationOptions Parse(string configuration, bool ignoreUnknow
heartbeatInterval = heartbeatInterval,
heartbeatConsistencyChecks = heartbeatConsistencyChecks,
highIntegrity = highIntegrity,
+ WriteMode = WriteMode,
+#if DEBUG
+ OutputLog = OutputLog,
+#endif
};
///
@@ -982,7 +990,9 @@ private void Clear()
CertificateSelection = null;
CertificateValidation = null;
ChannelPrefix = default;
+#pragma warning disable CS0618 // Type or member is obsolete
SocketManager = null;
+#pragma warning restore CS0618 // Type or member is obsolete
Tunnel = null;
}
@@ -1171,6 +1181,12 @@ private ConfigurationOptions DoParse(string configuration, bool ignoreUnknown)
///
public RedisProtocol? Protocol { get; set; }
+ internal BufferedStreamWriter.WriteMode WriteMode { get; set; }
+
+#if DEBUG
+ internal Action? OutputLog;
+#endif
+
internal bool TryResp3()
{
// note: deliberately leaving the IsAvailable duplicated to use short-circuit
diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.ReaderWriter.cs b/src/StackExchange.Redis/ConnectionMultiplexer.ReaderWriter.cs
deleted file mode 100644
index a30da7865..000000000
--- a/src/StackExchange.Redis/ConnectionMultiplexer.ReaderWriter.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-
-namespace StackExchange.Redis;
-
-public partial class ConnectionMultiplexer
-{
- internal SocketManager? SocketManager { get; private set; }
-
- [MemberNotNull(nameof(SocketManager))]
- private void OnCreateReaderWriter(ConfigurationOptions configuration)
- {
- SocketManager = configuration.SocketManager ?? SocketManager.Shared;
- }
-
- private void OnCloseReaderWriter()
- {
- SocketManager = null;
- }
-}
diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs
index a5995046e..74bbb55f2 100644
--- a/src/StackExchange.Redis/ConnectionMultiplexer.cs
+++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs
@@ -147,7 +147,6 @@ private ConnectionMultiplexer(ConfigurationOptions configuration, ServerType? se
map.AssertAvailable(RedisCommand.EXISTS);
}
- OnCreateReaderWriter(configuration);
ServerSelectionStrategy = new ServerSelectionStrategy(this);
var configChannel = configuration.ConfigurationChannel;
@@ -352,7 +351,7 @@ internal void CheckMessage(Message message)
}
// 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)
+ if (message.ArgCount >= MessageWriter.REDIS_MAX_ARGS)
{
throw ExceptionFactory.TooManyArgs(message.CommandAndKey, message.ArgCount);
}
@@ -2305,7 +2304,6 @@ public void Close(bool allowCommandsToComplete = true)
WaitAllIgnoreErrors(quits);
}
DisposeAndClearServers();
- OnCloseReaderWriter();
OnClosing(true);
Interlocked.Increment(ref _connectionCloseCount);
}
diff --git a/src/StackExchange.Redis/CursorEnumerable.cs b/src/StackExchange.Redis/CursorEnumerable.cs
index 921d83ce0..8f9863bb3 100644
--- a/src/StackExchange.Redis/CursorEnumerable.cs
+++ b/src/StackExchange.Redis/CursorEnumerable.cs
@@ -55,6 +55,17 @@ internal readonly struct ScanResult
public readonly T[]? ValuesOversized;
public readonly int Count;
public readonly bool IsPooled;
+ public ReadOnlySpan Values => ValuesOversized is null ? [] : ValuesOversized.AsSpan(0, Count);
+
+ public void Recycle()
+ {
+ if (IsPooled && ValuesOversized != null)
+ {
+ ArrayPool.Shared.Return(ValuesOversized);
+ }
+
+ Unsafe.AsRef(in this) = default; // best effort wipe
+ }
public ScanResult(RedisValue cursor, T[]? valuesOversized, int count, bool isPooled)
{
Cursor = cursor;
diff --git a/src/StackExchange.Redis/Enums/CommandFlags.cs b/src/StackExchange.Redis/Enums/CommandFlags.cs
index 83331a3c5..fc3c6070d 100644
--- a/src/StackExchange.Redis/Enums/CommandFlags.cs
+++ b/src/StackExchange.Redis/Enums/CommandFlags.cs
@@ -106,7 +106,7 @@ public enum CommandFlags
///
NoScriptCache = 512,
- // 1024: Removed - was used for async timeout checks; never user-specified, so not visible on the public API
+ // 1024: used for "no flush"; 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/Enums/RedisType.cs b/src/StackExchange.Redis/Enums/RedisType.cs
index 90a41165b..429dd1b9f 100644
--- a/src/StackExchange.Redis/Enums/RedisType.cs
+++ b/src/StackExchange.Redis/Enums/RedisType.cs
@@ -1,4 +1,7 @@
-namespace StackExchange.Redis
+using System;
+using RESPite;
+
+namespace StackExchange.Redis
{
///
/// The intrinsic data-types supported by redis.
@@ -9,6 +12,7 @@ public enum RedisType
///
/// The specified key does not exist.
///
+ [AsciiHash("")]
None,
///
@@ -17,6 +21,7 @@ public enum RedisType
/// A String value can be at max 512 Megabytes in length.
///
///
+ [AsciiHash("string")]
String,
///
@@ -25,6 +30,7 @@ public enum RedisType
/// on the tail (on the right) of the list.
///
///
+ [AsciiHash("list")]
List,
///
@@ -35,6 +41,7 @@ public enum RedisType
/// Practically speaking this means that adding a member does not require a check if exists then add operation.
///
///
+ [AsciiHash("set")]
Set,
///
@@ -44,6 +51,7 @@ public enum RedisType
/// While members are unique, scores may be repeated.
///
///
+ [AsciiHash("zset")]
SortedSet,
///
@@ -51,6 +59,7 @@ public enum RedisType
/// to represent objects (e.g. A User with a number of fields like name, surname, age, and so forth).
///
///
+ [AsciiHash("hash")]
Hash,
///
@@ -59,6 +68,7 @@ public enum RedisType
/// stream contains a unique message ID and a list of name/value pairs containing the entry's data.
///
///
+ [AsciiHash("stream")]
Stream,
///
@@ -70,6 +80,16 @@ public enum RedisType
/// 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.
///
+ [AsciiHash("vectorset")]
VectorSet,
}
+
+ ///
+ /// Metadata and parsing methods for RedisType.
+ ///
+ internal static partial class RedisTypeMetadata
+ {
+ [AsciiHash]
+ internal static partial bool TryParse(ReadOnlySpan value, out RedisType redisType);
+ }
}
diff --git a/src/StackExchange.Redis/Enums/ServerType.cs b/src/StackExchange.Redis/Enums/ServerType.cs
index ef49a8449..43bc087c5 100644
--- a/src/StackExchange.Redis/Enums/ServerType.cs
+++ b/src/StackExchange.Redis/Enums/ServerType.cs
@@ -43,7 +43,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 434abce17..a6e86036b 100644
--- a/src/StackExchange.Redis/ExceptionFactory.cs
+++ b/src/StackExchange.Redis/ExceptionFactory.cs
@@ -28,7 +28,7 @@ internal static Exception CommandDisabled(string command)
=> new RedisCommandException("This operation has been disabled in the command-map and cannot be used: " + 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}");
+ => new RedisCommandException($"This operation would involve too many arguments ({argCount + 1} vs the redis limit of {MessageWriter.REDIS_MAX_ARGS}): {command}");
internal static Exception ConnectionFailure(bool includeDetail, ConnectionFailureType failureType, string message, ServerEndPoint? server)
{
@@ -367,7 +367,6 @@ private static void AddCommonDetail(
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());
Add(data, sb, "Client-Name", "clientName", multiplexer.ClientName);
if (message != null)
diff --git a/src/StackExchange.Redis/Expiration.cs b/src/StackExchange.Redis/Expiration.cs
index e04094358..738b0b111 100644
--- a/src/StackExchange.Redis/Expiration.cs
+++ b/src/StackExchange.Redis/Expiration.cs
@@ -244,7 +244,7 @@ private static void ThrowMode(ExpirationMode mode) =>
_ => 2,
};
- internal void WriteTo(PhysicalConnection physical)
+ internal void WriteTo(in MessageWriter writer)
{
var mode = Mode;
switch (Mode)
@@ -252,13 +252,13 @@ internal void WriteTo(PhysicalConnection physical)
case ExpirationMode.Default or ExpirationMode.NotUsed:
break;
case ExpirationMode.KeepTtl:
- physical.WriteBulkString("KEEPTTL"u8);
+ writer.WriteBulkString("KEEPTTL"u8);
break;
case ExpirationMode.Persist:
- physical.WriteBulkString("PERSIST"u8);
+ writer.WriteBulkString("PERSIST"u8);
break;
default:
- physical.WriteBulkString(mode switch
+ writer.WriteBulkString(mode switch
{
ExpirationMode.RelativeSeconds => "EX"u8,
ExpirationMode.RelativeMilliseconds => "PX"u8,
@@ -266,7 +266,7 @@ internal void WriteTo(PhysicalConnection physical)
ExpirationMode.AbsoluteMilliseconds => "PXAT"u8,
_ => default,
});
- physical.WriteBulkString(Value);
+ writer.WriteBulkString(Value);
break;
}
}
diff --git a/src/StackExchange.Redis/ExtensionMethods.cs b/src/StackExchange.Redis/ExtensionMethods.cs
index e5a5c4d4d..240a8752b 100644
--- a/src/StackExchange.Redis/ExtensionMethods.cs
+++ b/src/StackExchange.Redis/ExtensionMethods.cs
@@ -8,7 +8,6 @@
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
-using Pipelines.Sockets.Unofficial.Arenas;
namespace StackExchange.Redis
{
@@ -329,13 +328,5 @@ internal static int VectorSafeIndexOfCRLF(this ReadOnlySpan span)
return -1;
}
#endif
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- 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)
- => result.IsNull ? null : result.GetItems().ToArray(selector, in state);
}
}
diff --git a/src/StackExchange.Redis/FrameworkShims.IsExternalInit.cs b/src/StackExchange.Redis/FrameworkShims.IsExternalInit.cs
new file mode 100644
index 000000000..417975050
--- /dev/null
+++ b/src/StackExchange.Redis/FrameworkShims.IsExternalInit.cs
@@ -0,0 +1,15 @@
+using System.ComponentModel;
+
+#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
+// ReSharper disable once CheckNamespace
+namespace System.Runtime.CompilerServices
+{
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ internal static class IsExternalInit { }
+}
+#endif
diff --git a/src/StackExchange.Redis/FrameworkShims.cs b/src/StackExchange.Redis/FrameworkShims.cs
deleted file mode 100644
index ce954406d..000000000
--- a/src/StackExchange.Redis/FrameworkShims.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-#pragma warning disable SA1403 // single namespace
-
-#if NET
-// 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 !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
-
-#if !NET
-
-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);
- }
- }
- }
-
- 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
-
-
-#pragma warning restore SA1403
diff --git a/src/StackExchange.Redis/HelloField.cs b/src/StackExchange.Redis/HelloField.cs
new file mode 100644
index 000000000..e5d730ce4
--- /dev/null
+++ b/src/StackExchange.Redis/HelloField.cs
@@ -0,0 +1,55 @@
+using System;
+using RESPite;
+
+namespace StackExchange.Redis;
+
+///
+/// Fields that can appear in a HELLO command response.
+///
+internal enum HelloField
+{
+ ///
+ /// Unknown or unrecognized field.
+ ///
+ [AsciiHash("")]
+ Unknown = 0,
+
+ ///
+ /// Redis server version.
+ ///
+ [AsciiHash("version")]
+ Version,
+
+ ///
+ /// Protocol version (2 or 3).
+ ///
+ [AsciiHash("proto")]
+ Proto,
+
+ ///
+ /// Connection ID.
+ ///
+ [AsciiHash("id")]
+ Id,
+
+ ///
+ /// Server mode (standalone, cluster, sentinel).
+ ///
+ [AsciiHash("mode")]
+ Mode,
+
+ ///
+ /// Server role (master/primary, slave/replica).
+ ///
+ [AsciiHash("role")]
+ Role,
+}
+
+///
+/// Metadata and parsing methods for HelloField.
+///
+internal static partial class HelloFieldMetadata
+{
+ [AsciiHash]
+ internal static partial bool TryParse(ReadOnlySpan value, out HelloField field);
+}
diff --git a/src/StackExchange.Redis/HotKeys.ResultProcessor.cs b/src/StackExchange.Redis/HotKeys.ResultProcessor.cs
index 71644c010..10b2d6357 100644
--- a/src/StackExchange.Redis/HotKeys.ResultProcessor.cs
+++ b/src/StackExchange.Redis/HotKeys.ResultProcessor.cs
@@ -1,4 +1,8 @@
-namespace StackExchange.Redis;
+using System;
+using RESPite;
+using RESPite.Messages;
+
+namespace StackExchange.Redis;
public sealed partial class HotKeysResult
{
@@ -6,21 +10,22 @@ public sealed partial class HotKeysResult
private sealed class HotKeysResultProcessor : ResultProcessor
{
- protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result)
+ protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader)
{
- if (result.IsNull)
+ if (reader.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 })
+ if (reader.IsAggregate && reader.AggregateLengthIs(1))
{
- ref readonly RawResult inner = ref result[0];
- if (inner is { Resp2TypeArray: ResultType.Array, IsNull: false })
+ var iter = reader.AggregateChildren();
+ iter.DemandNext();
+ if (iter.Value.IsAggregate && !iter.Value.IsNull)
{
- var hotKeys = new HotKeysResult(in inner);
+ var hotKeys = new HotKeysResult(ref iter.Value);
SetResult(message, hotKeys);
return true;
}
@@ -30,160 +35,146 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes
}
}
- private HotKeysResult(in RawResult result)
+ private HotKeysResult(ref RespReader reader)
{
var metrics = HotKeysMetrics.None; // we infer this from the keys present
- var iter = result.GetItems().GetEnumerator();
- while (iter.MoveNext())
+ int count = reader.AggregateLength();
+ if ((count & 1) != 0) return; // must be even (key-value pairs)
+
+ while (reader.TryMoveNext() && reader.IsScalar)
{
- if (!iter.Current.TryParse(HotKeysFieldMetadata.TryParse, out HotKeysField field))
- field = HotKeysField.Unknown;
+ HotKeysField field;
+ unsafe
+ {
+ if (!reader.TryParseScalar(&HotKeysFieldMetadata.TryParse, out field))
+ {
+ field = HotKeysField.Unknown;
+ }
+ }
- if (!iter.MoveNext()) break; // lies about the length!
- ref readonly RawResult value = ref iter.Current;
+ // Move to value
+ if (!reader.TryMoveNext()) break;
long i64;
switch (field)
{
case HotKeysField.TrackingActive:
- TrackingActive = value.GetBoolean();
+ TrackingActive = reader.ReadBoolean();
break;
- case HotKeysField.SampleRatio when value.TryGetInt64(out i64):
+ case HotKeysField.SampleRatio when reader.TryReadInt64(out i64):
SampleRatio = i64;
break;
- case HotKeysField.SelectedSlots when 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)
+ case HotKeysField.SelectedSlots when reader.IsAggregate:
+ var slotRanges = reader.ReadPastArray(
+ static (ref RespReader slotReader) =>
{
+ if (!slotReader.IsAggregate) return default;
+
+ int pairLen = slotReader.AggregateLength();
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)
+ var pairIter = slotReader.AggregateChildren();
+ if (pairLen >= 1 && pairIter.MoveNext() && pairIter.Value.TryReadInt64(out from))
{
- // this is the "normal" case when no slot filter was applied
- slots = SlotRange.SharedAllSlots; // avoid the alloc
+ to = from; // single slot
+ if (pairLen >= 2 && pairIter.MoveNext() && pairIter.Value.TryReadInt64(out to))
+ {
+ // to is now set
+ }
}
- else
- {
- slots ??= new SlotRange[len];
- slots[i] = new((int)from, (int)to);
- }
- }
+
+ return from >= SlotRange.MinSlot ? new SlotRange((int)from, (int)to) : default;
+ },
+ scalar: false);
+
+ if (slotRanges is { Length: 1 } && slotRanges[0].From == SlotRange.MinSlot && slotRanges[0].To == SlotRange.MaxSlot)
+ {
+ // this is the "normal" case when no slot filter was applied
+ _selectedSlots = SlotRange.SharedAllSlots; // avoid the alloc
+ }
+ else
+ {
+ _selectedSlots = slotRanges ?? [];
}
- _selectedSlots = slots;
break;
- case HotKeysField.AllCommandsAllSlotsUs when value.TryGetInt64(out i64):
+ case HotKeysField.AllCommandsAllSlotsUs when reader.TryReadInt64(out i64):
AllCommandsAllSlotsMicroseconds = i64;
break;
- case HotKeysField.AllCommandsSelectedSlotsUs when value.TryGetInt64(out i64):
+ case HotKeysField.AllCommandsSelectedSlotsUs when reader.TryReadInt64(out i64):
AllCommandSelectedSlotsMicroseconds = i64;
break;
- case HotKeysField.SampledCommandSelectedSlotsUs when value.TryGetInt64(out i64):
- case HotKeysField.SampledCommandsSelectedSlotsUs when value.TryGetInt64(out i64):
+ case HotKeysField.SampledCommandSelectedSlotsUs when reader.TryReadInt64(out i64):
+ case HotKeysField.SampledCommandsSelectedSlotsUs when reader.TryReadInt64(out i64):
SampledCommandsSelectedSlotsMicroseconds = i64;
break;
- case HotKeysField.NetBytesAllCommandsAllSlots when value.TryGetInt64(out i64):
+ case HotKeysField.NetBytesAllCommandsAllSlots when reader.TryReadInt64(out i64):
AllCommandsAllSlotsNetworkBytes = i64;
break;
- case HotKeysField.NetBytesAllCommandsSelectedSlots when value.TryGetInt64(out i64):
+ case HotKeysField.NetBytesAllCommandsSelectedSlots when reader.TryReadInt64(out i64):
NetworkBytesAllCommandsSelectedSlotsRaw = i64;
break;
- case HotKeysField.NetBytesSampledCommandsSelectedSlots when value.TryGetInt64(out i64):
+ case HotKeysField.NetBytesSampledCommandsSelectedSlots when reader.TryReadInt64(out i64):
NetworkBytesSampledCommandsSelectedSlotsRaw = i64;
break;
- case HotKeysField.CollectionStartTimeUnixMs when value.TryGetInt64(out i64):
+ case HotKeysField.CollectionStartTimeUnixMs when reader.TryReadInt64(out i64):
CollectionStartTimeUnixMilliseconds = i64;
break;
- case HotKeysField.CollectionDurationMs when value.TryGetInt64(out i64):
+ case HotKeysField.CollectionDurationMs when reader.TryReadInt64(out i64):
CollectionDurationMicroseconds = i64 * 1000; // ms vs us is in question: support both, and abstract it from the caller
break;
- case HotKeysField.CollectionDurationUs when value.TryGetInt64(out i64):
+ case HotKeysField.CollectionDurationUs when reader.TryReadInt64(out i64):
CollectionDurationMicroseconds = i64;
break;
- case HotKeysField.TotalCpuTimeSysMs when value.TryGetInt64(out i64):
+ case HotKeysField.TotalCpuTimeSysMs when reader.TryReadInt64(out i64):
metrics |= HotKeysMetrics.Cpu;
TotalCpuTimeSystemMicroseconds = i64 * 1000; // ms vs us is in question: support both, and abstract it from the caller
break;
- case HotKeysField.TotalCpuTimeSysUs when value.TryGetInt64(out i64):
+ case HotKeysField.TotalCpuTimeSysUs when reader.TryReadInt64(out i64):
metrics |= HotKeysMetrics.Cpu;
TotalCpuTimeSystemMicroseconds = i64;
break;
- case HotKeysField.TotalCpuTimeUserMs when value.TryGetInt64(out i64):
+ case HotKeysField.TotalCpuTimeUserMs when reader.TryReadInt64(out i64):
metrics |= HotKeysMetrics.Cpu;
TotalCpuTimeUserMicroseconds = i64 * 1000; // ms vs us is in question: support both, and abstract it from the caller
break;
- case HotKeysField.TotalCpuTimeUserUs when value.TryGetInt64(out i64):
+ case HotKeysField.TotalCpuTimeUserUs when reader.TryReadInt64(out i64):
metrics |= HotKeysMetrics.Cpu;
TotalCpuTimeUserMicroseconds = i64;
break;
- case HotKeysField.TotalNetBytes when value.TryGetInt64(out i64):
+ case HotKeysField.TotalNetBytes when reader.TryReadInt64(out i64):
metrics |= HotKeysMetrics.Network;
TotalNetworkBytesRaw = i64;
break;
- case HotKeysField.ByCpuTimeUs when value.Resp2TypeArray is ResultType.Array:
+ case HotKeysField.ByCpuTimeUs when reader.IsAggregate:
metrics |= HotKeysMetrics.Cpu;
- len = value.ItemsCount / 2;
- if (len == 0)
+ int cpuLen = reader.AggregateLength() / 2;
+ var cpuTime = new MetricKeyCpu[cpuLen];
+ var cpuIter = reader.AggregateChildren();
+ int cpuIdx = 0;
+ while (cpuIter.MoveNext() && cpuIdx < cpuLen)
{
- _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))
+ var metricKey = cpuIter.Value.ReadRedisKey();
+ if (cpuIter.MoveNext() && cpuIter.Value.TryReadInt64(out var metricValue))
{
- cpuTime[i] = new(metricKey, metricValue);
+ cpuTime[cpuIdx++] = new(metricKey, metricValue);
}
}
-
_cpuByKey = cpuTime;
break;
- case HotKeysField.ByNetBytes when value.Resp2TypeArray is ResultType.Array:
+ case HotKeysField.ByNetBytes when reader.IsAggregate:
metrics |= HotKeysMetrics.Network;
- len = value.ItemsCount / 2;
- if (len == 0)
+ int netLen = reader.AggregateLength() / 2;
+ var netBytes = new MetricKeyBytes[netLen];
+ var netIter = reader.AggregateChildren();
+ int netIdx = 0;
+ while (netIter.MoveNext() && netIdx < netLen)
{
- _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))
+ var metricKey = netIter.Value.ReadRedisKey();
+ if (netIter.MoveNext() && netIter.Value.TryReadInt64(out var metricValue))
{
- netBytes[i] = new(metricKey, metricValue);
+ netBytes[netIdx++] = new(metricKey, metricValue);
}
}
-
_networkBytesByKey = netBytes;
break;
} // switch
diff --git a/src/StackExchange.Redis/HotKeys.StartMessage.cs b/src/StackExchange.Redis/HotKeys.StartMessage.cs
index c9f0bc371..8793e79df 100644
--- a/src/StackExchange.Redis/HotKeys.StartMessage.cs
+++ b/src/StackExchange.Redis/HotKeys.StartMessage.cs
@@ -1,5 +1,4 @@
using System;
-using System.Threading.Tasks;
namespace StackExchange.Redis;
@@ -13,7 +12,7 @@ internal sealed class HotKeysStartMessage(
long sampleRatio,
int[]? slots) : Message(-1, flags, RedisCommand.HOTKEYS)
{
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
/*
HOTKEYS START
@@ -23,41 +22,41 @@ [DURATION duration]
[SAMPLE ratio]
[SLOTS count slot…]
*/
- physical.WriteHeader(Command, ArgCount);
- physical.WriteBulkString("START"u8);
- physical.WriteBulkString("METRICS"u8);
+ writer.WriteHeader(Command, ArgCount);
+ writer.WriteBulkString("START"u8);
+ writer.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);
+ writer.WriteBulkString(metricCount);
+ if ((metrics & HotKeysMetrics.Cpu) != 0) writer.WriteBulkString("CPU"u8);
+ if ((metrics & HotKeysMetrics.Network) != 0) writer.WriteBulkString("NET"u8);
if (count != 0)
{
- physical.WriteBulkString("COUNT"u8);
- physical.WriteBulkString(count);
+ writer.WriteBulkString("COUNT"u8);
+ writer.WriteBulkString(count);
}
if (duration != TimeSpan.Zero)
{
- physical.WriteBulkString("DURATION"u8);
- physical.WriteBulkString(Math.Ceiling(duration.TotalSeconds));
+ writer.WriteBulkString("DURATION"u8);
+ writer.WriteBulkString(Math.Ceiling(duration.TotalSeconds));
}
if (sampleRatio != 1)
{
- physical.WriteBulkString("SAMPLE"u8);
- physical.WriteBulkString(sampleRatio);
+ writer.WriteBulkString("SAMPLE"u8);
+ writer.WriteBulkString(sampleRatio);
}
if (slots is { Length: > 0 })
{
- physical.WriteBulkString("SLOTS"u8);
- physical.WriteBulkString(slots.Length);
+ writer.WriteBulkString("SLOTS"u8);
+ writer.WriteBulkString(slots.Length);
foreach (var slot in slots)
{
- physical.WriteBulkString(slot);
+ writer.WriteBulkString(slot);
}
}
}
diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs b/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs
index 8e6444ea8..bc54bb4c1 100644
--- a/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs
+++ b/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs
@@ -1,4 +1,3 @@
-using System;
using System.Diagnostics.CodeAnalysis;
using RESPite;
diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs
index c581470ca..9719861d8 100644
--- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs
+++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
-using System.IO;
using System.Net;
using System.Threading.Tasks;
using RESPite;
diff --git a/src/StackExchange.Redis/Interfaces/IServer.cs b/src/StackExchange.Redis/Interfaces/IServer.cs
index fdd3d6872..944bf63fd 100644
--- a/src/StackExchange.Redis/Interfaces/IServer.cs
+++ b/src/StackExchange.Redis/Interfaces/IServer.cs
@@ -815,7 +815,11 @@ internal static class IServerExtensions
/// 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
+ ///
+ /// For testing only: Check if the server can simulate connection failure.
+ ///
+ /// The server to check.
+ internal static bool CanSimulateConnectionFailure(this IServer server) =>
+ (server as RedisServer)?.CanSimulateConnectionFailure == true;
}
}
diff --git a/src/StackExchange.Redis/KeyNotification.cs b/src/StackExchange.Redis/KeyNotification.cs
index 08c157bc6..b8142cfe8 100644
--- a/src/StackExchange.Redis/KeyNotification.cs
+++ b/src/StackExchange.Redis/KeyNotification.cs
@@ -1,7 +1,6 @@
using System;
using System.Buffers;
using System.Buffers.Text;
-using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using RESPite;
@@ -41,8 +40,8 @@ public static bool TryParse(scoped in RedisChannel channel, scoped in RedisValue
var hashCS = AsciiHash.HashCS(prefix);
switch (hashCS)
{
- case KeyEventPrefix.HashCS when KeyEventPrefix.IsCS(prefix, hashCS):
case KeySpacePrefix.HashCS when KeySpacePrefix.IsCS(prefix, hashCS):
+ case KeyEventPrefix.HashCS when KeyEventPrefix.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)
{
diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs
index ad4efe916..89ba07c99 100644
--- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs
+++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs
@@ -1,5 +1,4 @@
-using System;
-using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using RESPite;
diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs
index 83fbb2f85..0d025dc6c 100644
--- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs
+++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs
@@ -1,6 +1,4 @@
-using System;
-
-// ReSharper disable once CheckNamespace
+// ReSharper disable once CheckNamespace
namespace StackExchange.Redis.KeyspaceIsolation;
internal sealed partial class KeyPrefixedDatabase
diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs
index 01fe28505..498159578 100644
--- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs
+++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.IO;
using System.Net;
namespace StackExchange.Redis.KeyspaceIsolation
diff --git a/src/StackExchange.Redis/LCSField.cs b/src/StackExchange.Redis/LCSField.cs
new file mode 100644
index 000000000..2e2dbd8d0
--- /dev/null
+++ b/src/StackExchange.Redis/LCSField.cs
@@ -0,0 +1,37 @@
+using System;
+using RESPite;
+
+namespace StackExchange.Redis;
+
+///
+/// Fields that can appear in an LCS (Longest Common Subsequence) command response.
+///
+internal enum LCSField
+{
+ ///
+ /// Unknown or unrecognized field.
+ ///
+ [AsciiHash("")]
+ Unknown = 0,
+
+ ///
+ /// The matches array.
+ ///
+ [AsciiHash("matches")]
+ Matches,
+
+ ///
+ /// The length value.
+ ///
+ [AsciiHash("len")]
+ Len,
+}
+
+///
+/// Metadata and parsing methods for LCSField.
+///
+internal static partial class LCSFieldMetadata
+{
+ [AsciiHash]
+ internal static partial bool TryParse(ReadOnlySpan value, out LCSField field);
+}
diff --git a/src/StackExchange.Redis/LoggerExtensions.cs b/src/StackExchange.Redis/LoggerExtensions.cs
index 4a43514e4..381ee4426 100644
--- a/src/StackExchange.Redis/LoggerExtensions.cs
+++ b/src/StackExchange.Redis/LoggerExtensions.cs
@@ -494,7 +494,7 @@ internal static void LogWithThreadPoolStats(this ILogger? log, string message)
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);
+ internal static partial void LogInformationResponse(this ILogger logger, string? bridgeName, string commandAndKey, string result);
[LoggerMessage(
Level = LogLevel.Information,
diff --git a/src/StackExchange.Redis/Message.ValueCondition.cs b/src/StackExchange.Redis/Message.ValueCondition.cs
index 53ddc651b..d6b2e79b5 100644
--- a/src/StackExchange.Redis/Message.ValueCondition.cs
+++ b/src/StackExchange.Redis/Message.ValueCondition.cs
@@ -1,6 +1,4 @@
-using System;
-
-namespace StackExchange.Redis;
+namespace StackExchange.Redis;
internal partial class Message
{
@@ -22,11 +20,11 @@ private sealed class KeyConditionMessage(
public override int ArgCount => 1 + _when.TokenCount;
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, ArgCount);
- physical.Write(Key);
- _when.WriteTo(physical);
+ writer.WriteHeader(Command, ArgCount);
+ writer.Write(Key);
+ _when.WriteTo(writer);
}
}
@@ -46,13 +44,13 @@ private sealed class KeyValueExpiryConditionMessage(
public override int ArgCount => 2 + _expiry.TokenCount + _when.TokenCount;
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, ArgCount);
- physical.Write(Key);
- physical.WriteBulkString(_value);
- _expiry.WriteTo(physical);
- _when.WriteTo(physical);
+ writer.WriteHeader(Command, ArgCount);
+ writer.Write(Key);
+ writer.WriteBulkString(_value);
+ _expiry.WriteTo(writer);
+ _when.WriteTo(writer);
}
}
}
diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs
index 0ffcf4256..27da02aa0 100644
--- a/src/StackExchange.Redis/Message.cs
+++ b/src/StackExchange.Redis/Message.cs
@@ -7,8 +7,12 @@
using System.Text;
using System.Threading;
using Microsoft.Extensions.Logging;
+using RESPite.Internal;
+using RESPite.Messages;
using StackExchange.Redis.Profiling;
+#pragma warning disable SA1117 // params all same line; just noise here
+
namespace StackExchange.Redis
{
internal sealed class LoggingMessage : Message
@@ -34,15 +38,12 @@ private LoggingMessage(ILogger log, Message tail) : base(tail.Db, tail.Flags, ta
public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => tail.GetHashSlot(serverSelectionStrategy);
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- try
- {
- var bridge = physical.BridgeCouldBeNull;
- log?.LogTrace($"{bridge?.Name}: Writing: {tail.CommandAndKey}");
- }
- catch { }
- tail.WriteTo(physical);
+#if VERBOSE
+ log?.LogTrace($"Writing: {tail.CommandAndKey}");
+#endif
+ tail.WriteTo(writer);
}
public override int ArgCount => tail.ArgCount;
@@ -55,30 +56,34 @@ internal abstract partial class Message : ICompletable
private uint _highIntegrityToken;
- internal const CommandFlags InternalCallFlag = (CommandFlags)128;
+ internal const CommandFlags
+ InternalCallFlag = (CommandFlags)128,
+ NoFlushFlag = (CommandFlags)1024;
protected RedisCommand command;
private const CommandFlags AskingFlag = (CommandFlags)32,
- ScriptUnavailableFlag = (CommandFlags)256,
- DemandSubscriptionConnection = (CommandFlags)2048;
+ ScriptUnavailableFlag = (CommandFlags)256,
+ DemandSubscriptionConnection = (CommandFlags)2048;
private const CommandFlags MaskPrimaryServerPreference = CommandFlags.DemandMaster
- | CommandFlags.DemandReplica
- | CommandFlags.PreferMaster
- | CommandFlags.PreferReplica;
+ | CommandFlags.DemandReplica
+ | CommandFlags.PreferMaster
+ | CommandFlags.PreferReplica;
private const CommandFlags UserSelectableFlags = CommandFlags.None
- | CommandFlags.DemandMaster
- | CommandFlags.DemandReplica
- | CommandFlags.PreferMaster
- | CommandFlags.PreferReplica
+ | CommandFlags.DemandMaster
+ | CommandFlags.DemandReplica
+ | CommandFlags.PreferMaster
+ | CommandFlags.PreferReplica
#pragma warning disable CS0618 // Type or member is obsolete
- | CommandFlags.HighPriority
+ | CommandFlags.HighPriority
#pragma warning restore CS0618
- | CommandFlags.FireAndForget
- | CommandFlags.NoRedirect
- | CommandFlags.NoScriptCache;
+ | CommandFlags.FireAndForget
+ | CommandFlags.NoRedirect
+ | CommandFlags.NoScriptCache
+ | NoFlushFlag; // we'll allow this one even though not advertised
+
private IResultBox? resultBox;
private ResultProcessor? resultProcessor;
@@ -230,37 +235,46 @@ public static Message Create(int db, CommandFlags flags, RedisCommand command)
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) =>
+ 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) =>
+ 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) =>
+ 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) =>
new CommandValueMessage(db, flags, command, value);
- public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue 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) =>
new CommandChannelMessage(db, flags, command, channel);
- public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisChannel channel, in RedisValue 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) =>
+ 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) =>
+ 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) =>
+ 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)
+ public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key,
+ GeoEntry[] values)
{
#if NET
ArgumentNullException.ThrowIfNull(values);
@@ -271,11 +285,13 @@ public static Message Create(int db, CommandFlags flags, RedisCommand command, i
{
throw new ArgumentOutOfRangeException(nameof(values));
}
+
if (values.Length == 1)
{
var value = values[0];
return Create(db, flags, command, key, value.Longitude, value.Latitude, value.Member);
}
+
var arr = new RedisValue[3 * values.Length];
int index = 0;
foreach (var value in values)
@@ -284,34 +300,50 @@ public static Message Create(int db, CommandFlags flags, RedisCommand command, i
arr[index++] = value.Latitude;
arr[index++] = value.Member;
}
+
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) =>
+ 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 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);
-
- public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisValue value0, in RedisValue value1) =>
+ 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);
+
+ 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) =>
+ 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) =>
+ 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) =>
+ 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) =>
+ 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(
@@ -345,7 +377,8 @@ public static Message Create(
in RedisValue value1,
in RedisValue value2,
in RedisValue value3) =>
- new CommandKeyKeyValueValueValueValueMessage(db, flags, command, key0, key1, value0, value1, value2, value3);
+ new CommandKeyKeyValueValueValueValueMessage(db, flags, command, key0, key1, value0, value1, value2,
+ value3);
public static Message Create(
int db,
@@ -358,7 +391,8 @@ public static Message Create(
in RedisValue value2,
in RedisValue value3,
in RedisValue value4) =>
- new CommandKeyKeyValueValueValueValueValueMessage(db, flags, command, key0, key1, value0, value1, value2, value3, value4);
+ new CommandKeyKeyValueValueValueValueValueMessage(db, flags, command, key0, key1, value0, value1, value2,
+ value3, value4);
public static Message Create(
int db,
@@ -372,7 +406,8 @@ public static Message Create(
in RedisValue value3,
in RedisValue value4,
in RedisValue value5) =>
- new CommandKeyKeyValueValueValueValueValueValueMessage(db, flags, command, key0, key1, value0, value1, value2, value3, value4, value5);
+ new CommandKeyKeyValueValueValueValueValueValueMessage(db, flags, command, key0, key1, value0, value1,
+ value2, value3, value4, value5);
public static Message Create(
int db,
@@ -387,12 +422,15 @@ public static Message Create(
in RedisValue value4,
in RedisValue value5,
in RedisValue value6) =>
- new CommandKeyKeyValueValueValueValueValueValueValueMessage(db, flags, command, key0, key1, value0, value1, value2, value3, value4, value5, 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) =>
+ 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)
+ 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.
@@ -409,7 +447,8 @@ public virtual void AppendStormLog(StringBuilder sb)
sb.Append(CommandAndKey);
}
- public virtual int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => ServerSelectionStrategy.NoSlot;
+ public virtual int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) =>
+ ServerSelectionStrategy.NoSlot;
///
/// This does a few important things:
@@ -455,7 +494,8 @@ public void Complete()
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
+ internal static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key,
+ RedisKey[] keys) => keys.Length switch
{
0 => new CommandKeyMessage(db, flags, command, key),
1 => new CommandKeyKeyMessage(db, flags, command, key, keys[0]),
@@ -463,27 +503,31 @@ public void Complete()
_ => new CommandKeyKeysMessage(db, flags, command, key, keys),
};
- internal static Message Create(int db, CommandFlags flags, RedisCommand command, IList keys) => keys.Count switch
- {
- 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 keys) =>
+ keys.Count switch
+ {
+ 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) => values.Count switch
- {
- 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, IList values) =>
+ values.Count switch
+ {
+ 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)
+ internal static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key,
+ RedisValue[] values)
{
#if NET
ArgumentNullException.ThrowIfNull(values);
@@ -496,12 +540,14 @@ internal static Message Create(int db, CommandFlags flags, RedisCommand command,
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]),
+ 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, in RedisKey key1, RedisValue[] values)
+ internal static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0,
+ in RedisKey key1, RedisValue[] values)
{
#if NET
ArgumentNullException.ThrowIfNull(values);
@@ -513,16 +559,22 @@ internal static Message Create(int db, CommandFlags flags, RedisCommand command,
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]),
+ 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)
+ internal static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0,
+ RedisValue[] values, in RedisKey key1)
{
#if NET
ArgumentNullException.ThrowIfNull(values);
@@ -594,34 +646,55 @@ internal static bool RequiresDatabase(RedisCommand command)
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))
- | primaryReplica;
+ return (everything & ~(CommandFlags.DemandMaster | CommandFlags.DemandReplica | CommandFlags.PreferMaster |
+ CommandFlags.PreferReplica))
+ | primaryReplica;
}
internal void Cancel() => resultBox?.Cancel();
// true if ready to be completed (i.e. false if re-issued to another server)
- internal bool ComputeResult(PhysicalConnection connection, in RawResult result)
+ internal bool ComputeResult(PhysicalConnection connection, ref RespReader reader)
{
+ var prefix = RespPrefix.None;
+
+ // intentionally "frame" is an isolated copy
var box = resultBox;
try
{
- if (box != null && box.IsFaulted) return false; // already failed (timeout, etc)
- if (resultProcessor == null) return true;
+ // we don't want to mutate reader, so that processors can consume attributes; however,
+ // we also don't want to force the entire reader to copy each time, so: snapshot
+ // just the prefix
+ prefix = reader.GetFirstPrefix();
+
+ if (box != null && box.IsFaulted)
+ {
+ connection.OnDetailLog($"already faulted for {Command}");
+ return false; // already failed (timeout, etc)
+ }
+
+ if (resultProcessor == null)
+ {
+ connection.OnDetailLog($"no result processor for {Command}");
+ return true;
+ }
// false here would be things like resends (MOVED) - the message is not yet complete
- return resultProcessor.SetResult(connection, this, result);
+ connection.OnDetailLog($"computing result for {Command} with {resultProcessor.GetType().Name}");
+ return resultProcessor.SetResult(connection, this, ref reader);
}
catch (Exception ex)
{
- ex.Data.Add("got", result.ToString());
+ connection.OnDetailLog($"{ex.GetType().Name}: {ex.Message}");
+ ex.Data.Add("got", prefix.ToString());
connection?.BridgeCouldBeNull?.Multiplexer?.OnMessageFaulted(this, ex);
box?.SetException(ex);
return box != null; // we still want to pulse/complete
}
}
- 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);
@@ -640,6 +713,7 @@ internal bool TrySetResult(T value)
typed.SetResult(value);
return true;
}
+
return false;
}
@@ -707,8 +781,11 @@ internal void SetRequestSent()
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void SetWriteTime()
{
- _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);
@@ -720,6 +797,11 @@ internal void SetWriteTime()
public virtual string CommandString => Command.ToString();
+ internal bool IsFlushRequiredAsync => (Flags & NoFlushFlag) == 0;
+
+ // for sync to skip flush, we need *both* NoFlush and FireAndForget; we absolutely need to flush if someone is doing a sync call
+ internal bool IsFlushRequiredSync => (Flags & (NoFlushFlag | CommandFlags.FireAndForget)) != (NoFlushFlag | CommandFlags.FireAndForget);
+
///
/// Sends this command to the subscription connection rather than the interactive.
///
@@ -775,13 +857,46 @@ internal void SetSource(IResultBox resultBox, ResultProcessor? resultPr
this.resultProcessor = resultProcessor;
}
- protected abstract void WriteImpl(PhysicalConnection physical);
+ internal void WriteTo(in MessageWriter writer) => WriteImpl(in writer);
+ protected abstract void WriteImpl(in MessageWriter writer);
+
+ internal string GetRespString(PhysicalConnection connection)
+ {
+ MessageWriter writer = new MessageWriter(connection, BlockBufferSerializer.Shared);
+ try
+ {
+ WriteImpl(in writer);
+ var bytes = MessageWriter.FlushBlockBuffer();
+ string s = Encoding.UTF8.GetString(bytes.Span);
+ MessageWriter.ReleaseBlockBuffer(bytes);
+ return s;
+ }
+ finally
+ {
+ MessageWriter.RevertBlockBuffer();
+ }
+ }
internal void WriteTo(PhysicalConnection physical)
{
+ MessageWriter writer = new MessageWriter(physical, physical.Output);
try
{
- WriteImpl(physical);
+ WriteImpl(in writer);
+ }
+ catch (Exception ex) when (ex is not RedisCommandException) // these have specific meaning; don't wrap
+ {
+ physical?.OnInternalError(ex);
+ Fail(ConnectionFailureType.InternalFailure, ex, null, physical?.BridgeCouldBeNull?.Multiplexer);
+ }
+ }
+
+ internal void WriteTo(PhysicalConnection physical, CommandMap commandMap, byte[]? channelPrefix)
+ {
+ MessageWriter writer = new MessageWriter(channelPrefix, commandMap, physical.Output);
+ try
+ {
+ WriteImpl(in writer);
}
catch (Exception ex) when (ex is not RedisCommandException) // these have specific meaning; don't wrap
{
@@ -795,15 +910,16 @@ internal void WriteTo(PhysicalConnection physical)
internal void WriteHighIntegrityChecksumRequest(PhysicalConnection physical)
{
Debug.Assert(IsHighIntegrity, "should only be used for high-integrity");
+ var writer = new MessageWriter(physical, physical.Output);
try
{
- physical.WriteHeader(RedisCommand.ECHO, 1); // use WriteHeader to allow command-rewrite
+ writer.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);
+ writer.WriteRaw(chk);
}
catch (Exception ex)
{
@@ -841,20 +957,20 @@ public override int ArgCount
return count;
}
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, ArgCount);
- physical.WriteBulkString(_protocolVersion);
+ writer.WriteHeader(Command, ArgCount);
+ writer.WriteBulkString(_protocolVersion);
if (!string.IsNullOrWhiteSpace(_password))
{
- physical.WriteBulkString("AUTH"u8);
- physical.WriteBulkString(string.IsNullOrWhiteSpace(_username) ? RedisLiterals.@default : _username);
- physical.WriteBulkString(_password);
+ writer.WriteBulkString("AUTH"u8);
+ writer.WriteBulkString(string.IsNullOrWhiteSpace(_username) ? RedisLiterals.@default : _username);
+ writer.WriteBulkString(_password);
}
if (!string.IsNullOrWhiteSpace(_clientName))
{
- physical.WriteBulkString("SETNAME"u8);
- physical.WriteBulkString(_clientName);
+ writer.WriteBulkString("SETNAME"u8);
+ writer.WriteBulkString(_clientName);
}
}
}
@@ -895,10 +1011,10 @@ private sealed class CommandChannelMessage : CommandChannelBase
public CommandChannelMessage(int db, CommandFlags flags, RedisCommand command, in RedisChannel channel)
: base(db, flags, command, channel)
{ }
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, 1);
- physical.Write(Channel);
+ writer.WriteHeader(Command, 1);
+ writer.Write(Channel);
}
public override int ArgCount => 1;
}
@@ -913,11 +1029,11 @@ public CommandChannelValueMessage(int db, CommandFlags flags, RedisCommand comma
this.value = value;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, 2);
- physical.Write(Channel);
- physical.WriteBulkString(value);
+ writer.WriteHeader(Command, 2);
+ writer.Write(Channel);
+ writer.WriteBulkString(value);
}
public override int ArgCount => 2;
}
@@ -940,12 +1056,12 @@ public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy)
return serverSelectionStrategy.CombineSlot(slot, key2);
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, 3);
- physical.Write(Key);
- physical.Write(key1);
- physical.Write(key2);
+ writer.WriteHeader(Command, 3);
+ writer.Write(Key);
+ writer.Write(key1);
+ writer.Write(key2);
}
public override int ArgCount => 3;
}
@@ -965,11 +1081,11 @@ public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy)
return serverSelectionStrategy.CombineSlot(slot, key1);
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, 2);
- physical.Write(Key);
- physical.Write(key1);
+ writer.WriteHeader(Command, 2);
+ writer.Write(Key);
+ writer.Write(key1);
}
public override int ArgCount => 2;
}
@@ -996,13 +1112,13 @@ public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy)
return slot;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(command, keys.Length + 1);
- physical.Write(Key);
+ writer.WriteHeader(command, keys.Length + 1);
+ writer.Write(Key);
for (int i = 0; i < keys.Length; i++)
{
- physical.Write(keys[i]);
+ writer.Write(keys[i]);
}
}
public override int ArgCount => keys.Length + 1;
@@ -1017,12 +1133,12 @@ public CommandKeyKeyValueMessage(int db, CommandFlags flags, RedisCommand comman
this.value = value;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, 3);
- physical.Write(Key);
- physical.Write(key1);
- physical.WriteBulkString(value);
+ writer.WriteHeader(Command, 3);
+ writer.Write(Key);
+ writer.Write(key1);
+ writer.WriteBulkString(value);
}
public override int ArgCount => 3;
@@ -1032,10 +1148,10 @@ private sealed class CommandKeyMessage : CommandKeyBase
{
public CommandKeyMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key) : base(db, flags, command, key)
{ }
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, 1);
- physical.Write(Key);
+ writer.WriteHeader(Command, 1);
+ writer.Write(Key);
}
public override int ArgCount => 1;
}
@@ -1052,12 +1168,12 @@ public CommandValuesMessage(int db, CommandFlags flags, RedisCommand command, Re
this.values = values;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(command, values.Length);
+ writer.WriteHeader(command, values.Length);
for (int i = 0; i < values.Length; i++)
{
- physical.WriteBulkString(values[i]);
+ writer.WriteBulkString(values[i]);
}
}
public override int ArgCount => values.Length;
@@ -1085,12 +1201,12 @@ public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy)
return slot;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(command, keys.Length);
+ writer.WriteHeader(command, keys.Length);
for (int i = 0; i < keys.Length; i++)
{
- physical.Write(keys[i]);
+ writer.Write(keys[i]);
}
}
public override int ArgCount => keys.Length;
@@ -1105,11 +1221,11 @@ public CommandKeyValueMessage(int db, CommandFlags flags, RedisCommand command,
this.value = value;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, 2);
- physical.Write(Key);
- physical.WriteBulkString(value);
+ writer.WriteHeader(Command, 2);
+ writer.Write(Key);
+ writer.WriteBulkString(value);
}
public override int ArgCount => 2;
}
@@ -1135,12 +1251,12 @@ public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy)
return serverSelectionStrategy.CombineSlot(slot, key1);
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, values.Length + 2);
- physical.Write(Key);
- for (int i = 0; i < values.Length; i++) physical.WriteBulkString(values[i]);
- physical.Write(key1);
+ writer.WriteHeader(Command, values.Length + 2);
+ writer.Write(Key);
+ for (int i = 0; i < values.Length; i++) writer.WriteBulkString(values[i]);
+ writer.Write(key1);
}
public override int ArgCount => values.Length + 2;
}
@@ -1157,11 +1273,11 @@ public CommandKeyValuesMessage(int db, CommandFlags flags, RedisCommand command,
this.values = values;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, values.Length + 1);
- physical.Write(Key);
- for (int i = 0; i < values.Length; i++) physical.WriteBulkString(values[i]);
+ writer.WriteHeader(Command, values.Length + 1);
+ writer.Write(Key);
+ for (int i = 0; i < values.Length; i++) writer.WriteBulkString(values[i]);
}
public override int ArgCount => values.Length + 1;
}
@@ -1182,12 +1298,12 @@ public CommandKeyKeyValuesMessage(int db, CommandFlags flags, RedisCommand comma
this.values = values;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, values.Length + 2);
- physical.Write(Key);
- physical.Write(key1);
- for (int i = 0; i < values.Length; i++) physical.WriteBulkString(values[i]);
+ writer.WriteHeader(Command, values.Length + 2);
+ writer.Write(Key);
+ writer.Write(key1);
+ for (int i = 0; i < values.Length; i++) writer.WriteBulkString(values[i]);
}
public override int ArgCount => values.Length + 1;
}
@@ -1211,13 +1327,13 @@ public CommandKeyValueValueValuesMessage(int db, CommandFlags flags, RedisComman
this.values = values;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- 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]);
+ writer.WriteHeader(Command, values.Length + 3);
+ writer.Write(Key);
+ writer.WriteBulkString(value0);
+ writer.WriteBulkString(value1);
+ for (int i = 0; i < values.Length; i++) writer.WriteBulkString(values[i]);
}
public override int ArgCount => values.Length + 3;
}
@@ -1233,12 +1349,12 @@ public CommandKeyValueValueMessage(int db, CommandFlags flags, RedisCommand comm
this.value1 = value1;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, 3);
- physical.Write(Key);
- physical.WriteBulkString(value0);
- physical.WriteBulkString(value1);
+ writer.WriteHeader(Command, 3);
+ writer.Write(Key);
+ writer.WriteBulkString(value0);
+ writer.WriteBulkString(value1);
}
public override int ArgCount => 3;
}
@@ -1256,13 +1372,13 @@ public CommandKeyValueValueValueMessage(int db, CommandFlags flags, RedisCommand
this.value2 = value2;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, 4);
- physical.Write(Key);
- physical.WriteBulkString(value0);
- physical.WriteBulkString(value1);
- physical.WriteBulkString(value2);
+ writer.WriteHeader(Command, 4);
+ writer.Write(Key);
+ writer.WriteBulkString(value0);
+ writer.WriteBulkString(value1);
+ writer.WriteBulkString(value2);
}
public override int ArgCount => 4;
}
@@ -1282,14 +1398,14 @@ public CommandKeyValueValueValueValueMessage(int db, CommandFlags flags, RedisCo
this.value3 = value3;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, 5);
- physical.Write(Key);
- physical.WriteBulkString(value0);
- physical.WriteBulkString(value1);
- physical.WriteBulkString(value2);
- physical.WriteBulkString(value3);
+ writer.WriteHeader(Command, 5);
+ writer.Write(Key);
+ writer.WriteBulkString(value0);
+ writer.WriteBulkString(value1);
+ writer.WriteBulkString(value2);
+ writer.WriteBulkString(value3);
}
public override int ArgCount => 5;
}
@@ -1311,15 +1427,15 @@ public CommandKeyValueValueValueValueValueMessage(int db, CommandFlags flags, Re
this.value4 = value4;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, 6);
- physical.Write(Key);
- physical.WriteBulkString(value0);
- physical.WriteBulkString(value1);
- physical.WriteBulkString(value2);
- physical.WriteBulkString(value3);
- physical.WriteBulkString(value4);
+ writer.WriteHeader(Command, 6);
+ writer.Write(Key);
+ writer.WriteBulkString(value0);
+ writer.WriteBulkString(value1);
+ writer.WriteBulkString(value2);
+ writer.WriteBulkString(value3);
+ writer.WriteBulkString(value4);
}
public override int ArgCount => 6;
}
@@ -1344,16 +1460,16 @@ public CommandKeyValueValueValueValueValueValueMessage(int db, CommandFlags flag
this.value5 = value5;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- 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);
+ writer.WriteHeader(Command, ArgCount);
+ writer.Write(Key);
+ writer.WriteBulkString(value0);
+ writer.WriteBulkString(value1);
+ writer.WriteBulkString(value2);
+ writer.WriteBulkString(value3);
+ writer.WriteBulkString(value4);
+ writer.WriteBulkString(value5);
}
public override int ArgCount => 7;
}
@@ -1380,17 +1496,17 @@ public CommandKeyValueValueValueValueValueValueValueMessage(int db, CommandFlags
this.value6 = value6;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- 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);
+ writer.WriteHeader(Command, ArgCount);
+ writer.Write(Key);
+ writer.WriteBulkString(value0);
+ writer.WriteBulkString(value1);
+ writer.WriteBulkString(value2);
+ writer.WriteBulkString(value3);
+ writer.WriteBulkString(value4);
+ writer.WriteBulkString(value5);
+ writer.WriteBulkString(value6);
}
public override int ArgCount => 8;
}
@@ -1417,13 +1533,13 @@ public CommandKeyKeyValueValueMessage(
this.value1 = value1;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, ArgCount);
- physical.Write(Key);
- physical.Write(key1);
- physical.WriteBulkString(value0);
- physical.WriteBulkString(value1);
+ writer.WriteHeader(Command, ArgCount);
+ writer.Write(Key);
+ writer.Write(key1);
+ writer.WriteBulkString(value0);
+ writer.WriteBulkString(value1);
}
public override int ArgCount => 4;
@@ -1454,14 +1570,14 @@ public CommandKeyKeyValueValueValueMessage(
this.value2 = value2;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, ArgCount);
- physical.Write(Key);
- physical.Write(key1);
- physical.WriteBulkString(value0);
- physical.WriteBulkString(value1);
- physical.WriteBulkString(value2);
+ writer.WriteHeader(Command, ArgCount);
+ writer.Write(Key);
+ writer.Write(key1);
+ writer.WriteBulkString(value0);
+ writer.WriteBulkString(value1);
+ writer.WriteBulkString(value2);
}
public override int ArgCount => 5;
@@ -1495,15 +1611,15 @@ public CommandKeyKeyValueValueValueValueMessage(
this.value3 = value3;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, ArgCount);
- physical.Write(Key);
- physical.Write(key1);
- physical.WriteBulkString(value0);
- physical.WriteBulkString(value1);
- physical.WriteBulkString(value2);
- physical.WriteBulkString(value3);
+ writer.WriteHeader(Command, ArgCount);
+ writer.Write(Key);
+ writer.Write(key1);
+ writer.WriteBulkString(value0);
+ writer.WriteBulkString(value1);
+ writer.WriteBulkString(value2);
+ writer.WriteBulkString(value3);
}
public override int ArgCount => 6;
@@ -1540,16 +1656,16 @@ public CommandKeyKeyValueValueValueValueValueMessage(
this.value4 = value4;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- 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);
+ writer.WriteHeader(Command, ArgCount);
+ writer.Write(Key);
+ writer.Write(key1);
+ writer.WriteBulkString(value0);
+ writer.WriteBulkString(value1);
+ writer.WriteBulkString(value2);
+ writer.WriteBulkString(value3);
+ writer.WriteBulkString(value4);
}
public override int ArgCount => 7;
@@ -1589,17 +1705,17 @@ public CommandKeyKeyValueValueValueValueValueValueMessage(
this.value5 = value5;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- 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);
+ writer.WriteHeader(Command, ArgCount);
+ writer.Write(Key);
+ writer.Write(key1);
+ writer.WriteBulkString(value0);
+ writer.WriteBulkString(value1);
+ writer.WriteBulkString(value2);
+ writer.WriteBulkString(value3);
+ writer.WriteBulkString(value4);
+ writer.WriteBulkString(value5);
}
public override int ArgCount => 8;
@@ -1642,18 +1758,18 @@ public CommandKeyKeyValueValueValueValueValueValueValueMessage(
this.value6 = value6;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- 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);
+ writer.WriteHeader(Command, ArgCount);
+ writer.Write(Key);
+ writer.Write(key1);
+ writer.WriteBulkString(value0);
+ writer.WriteBulkString(value1);
+ writer.WriteBulkString(value2);
+ writer.WriteBulkString(value3);
+ writer.WriteBulkString(value4);
+ writer.WriteBulkString(value5);
+ writer.WriteBulkString(value6);
}
public override int ArgCount => 9;
@@ -1662,9 +1778,9 @@ protected override void WriteImpl(PhysicalConnection physical)
private sealed class CommandMessage : Message
{
public CommandMessage(int db, CommandFlags flags, RedisCommand command) : base(db, flags, command) { }
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, 0);
+ writer.WriteHeader(Command, 0);
}
public override int ArgCount => 0;
}
@@ -1687,12 +1803,12 @@ public CommandSlotValuesMessage(int db, int slot, CommandFlags flags, RedisComma
public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => slot;
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(command, values.Length);
+ writer.WriteHeader(command, values.Length);
for (int i = 0; i < values.Length; i++)
{
- physical.WriteBulkString(values[i]);
+ writer.WriteBulkString(values[i]);
}
}
public override int ArgCount => values.Length;
@@ -1718,29 +1834,29 @@ public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy)
? (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)
+ protected override void WriteImpl(in MessageWriter writer)
{
var cmd = Command;
- physical.WriteHeader(cmd, ArgCount);
+ writer.WriteHeader(cmd, ArgCount);
if (cmd == RedisCommand.MSETEX) // need count prefix
{
- physical.WriteBulkString(values.Length);
+ writer.WriteBulkString(values.Length);
}
for (int i = 0; i < values.Length; i++)
{
- physical.Write(values[i].Key);
- physical.WriteBulkString(values[i].Value);
+ writer.Write(values[i].Key);
+ writer.WriteBulkString(values[i].Value);
}
if (cmd == RedisCommand.MSETEX) // allow expiry/mode tokens
{
- expiry.WriteTo(physical);
+ expiry.WriteTo(writer);
switch (when)
{
case When.Exists:
- physical.WriteBulkString("XX"u8);
+ writer.WriteBulkString("XX"u8);
break;
case When.NotExists:
- physical.WriteBulkString("NX"u8);
+ writer.WriteBulkString("NX"u8);
break;
}
}
@@ -1757,11 +1873,11 @@ public CommandValueChannelMessage(int db, CommandFlags flags, RedisCommand comma
this.value = value;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, 2);
- physical.WriteBulkString(value);
- physical.Write(Channel);
+ writer.WriteHeader(Command, 2);
+ writer.WriteBulkString(value);
+ writer.Write(Channel);
}
public override int ArgCount => 2;
}
@@ -1782,11 +1898,11 @@ public override void AppendStormLog(StringBuilder sb)
sb.Append(" (").Append((string?)value).Append(')');
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, 2);
- physical.WriteBulkString(value);
- physical.Write(Key);
+ writer.WriteHeader(Command, 2);
+ writer.WriteBulkString(value);
+ writer.Write(Key);
}
public override int ArgCount => 2;
}
@@ -1800,10 +1916,10 @@ public CommandValueMessage(int db, CommandFlags flags, RedisCommand command, in
this.value = value;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, 1);
- physical.WriteBulkString(value);
+ writer.WriteHeader(Command, 1);
+ writer.WriteBulkString(value);
}
public override int ArgCount => 1;
}
@@ -1819,11 +1935,11 @@ public CommandValueValueMessage(int db, CommandFlags flags, RedisCommand command
this.value1 = value1;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, 2);
- physical.WriteBulkString(value0);
- physical.WriteBulkString(value1);
+ writer.WriteHeader(Command, 2);
+ writer.WriteBulkString(value0);
+ writer.WriteBulkString(value1);
}
public override int ArgCount => 2;
}
@@ -1841,12 +1957,12 @@ public CommandValueValueValueMessage(int db, CommandFlags flags, RedisCommand co
this.value2 = value2;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, 3);
- physical.WriteBulkString(value0);
- physical.WriteBulkString(value1);
- physical.WriteBulkString(value2);
+ writer.WriteHeader(Command, 3);
+ writer.WriteBulkString(value0);
+ writer.WriteBulkString(value1);
+ writer.WriteBulkString(value2);
}
public override int ArgCount => 3;
}
@@ -1868,14 +1984,14 @@ public CommandValueValueValueValueValueMessage(int db, CommandFlags flags, Redis
this.value4 = value4;
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, 5);
- physical.WriteBulkString(value0);
- physical.WriteBulkString(value1);
- physical.WriteBulkString(value2);
- physical.WriteBulkString(value3);
- physical.WriteBulkString(value4);
+ writer.WriteHeader(Command, 5);
+ writer.WriteBulkString(value0);
+ writer.WriteBulkString(value1);
+ writer.WriteBulkString(value2);
+ writer.WriteBulkString(value3);
+ writer.WriteBulkString(value4);
}
public override int ArgCount => 5;
}
@@ -1886,10 +2002,10 @@ public SelectMessage(int db, CommandFlags flags) : base(db, flags, RedisCommand.
{
}
- protected override void WriteImpl(PhysicalConnection physical)
+ protected override void WriteImpl(in MessageWriter writer)
{
- physical.WriteHeader(Command, 1);
- physical.WriteBulkString(Db);
+ writer.WriteHeader(Command, 1);
+ writer.WriteBulkString(Db);
}
public override int ArgCount => 1;
}
@@ -1901,7 +2017,9 @@ 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");
+ protected override void WriteImpl(in MessageWriter writer) => throw new InvalidOperationException("This message cannot be written");
}
+
+ public void SetNoFlush() => Flags |= NoFlushFlag;
}
}
diff --git a/src/StackExchange.Redis/MessageWriter.cs b/src/StackExchange.Redis/MessageWriter.cs
new file mode 100644
index 000000000..38269feef
--- /dev/null
+++ b/src/StackExchange.Redis/MessageWriter.cs
@@ -0,0 +1,602 @@
+using System;
+using System.Buffers;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+using RESPite;
+using RESPite.Internal;
+using RESPite.Messages;
+
+namespace StackExchange.Redis;
+
+internal readonly ref struct MessageWriter
+{
+ private readonly CommandMap _map;
+ private readonly byte[]? _channelPrefix;
+
+ public MessageWriter(byte[]? channelPrefix, CommandMap? map, IBufferWriter writer)
+ {
+ // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
+ _map = map ?? CommandMap.Default;
+ _channelPrefix = channelPrefix;
+ _writer = writer;
+ }
+
+ public static IBufferWriter BlockBuffer => BlockBufferSerializer.Shared;
+
+ public MessageWriter(PhysicalConnection connection, IBufferWriter writer)
+ {
+ if (connection.BridgeCouldBeNull is { } bridge)
+ {
+ _map = bridge.Multiplexer.CommandMap;
+ _channelPrefix = connection.ChannelPrefix;
+ }
+ else
+ {
+ _map = CommandMap.Default;
+ _channelPrefix = null;
+ }
+
+ _writer = writer ?? connection.Output;
+ }
+
+ private readonly IBufferWriter _writer;
+
+ public static ReadOnlyMemory FlushBlockBuffer() =>
+ BlockBufferSerializer.BlockBuffer.FinalizeMessage(BlockBufferSerializer.Shared);
+
+ public static void RevertBlockBuffer() => BlockBufferSerializer.Shared.Revert();
+
+ public static void ReleaseBlockBuffer(ReadOnlyMemory memory)
+ {
+ if (MemoryMarshal.TryGetMemoryManager(
+ memory, out var block))
+ {
+ block.Release();
+ }
+ }
+
+ public static void ReleaseBlockBuffer(in ReadOnlySequence request) =>
+ BlockBufferSerializer.BlockBuffer.Release(in request);
+
+ public void Write(in RedisKey key)
+ {
+ var val = key.KeyValue;
+ if (val is string s)
+ {
+ WriteUnifiedPrefixedString(_writer, key.KeyPrefix, s);
+ }
+ else
+ {
+ WriteUnifiedPrefixedBlob(_writer, key.KeyPrefix, (byte[]?)val);
+ }
+ }
+
+ internal void Write(in RedisChannel channel)
+ => WriteUnifiedPrefixedBlob(_writer, channel.IgnoreChannelPrefix ? null : _channelPrefix, channel.Value);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal void WriteBulkString(in RedisValue value)
+ => WriteBulkString(value, _writer);
+
+ internal static void WriteBulkString(in RedisValue value, IBufferWriter writer)
+ {
+ switch (value.Type)
+ {
+ case RedisValue.StorageType.Null:
+ WriteUnifiedBlob(writer, (byte[]?)null);
+ break;
+ case RedisValue.StorageType.Int64:
+ WriteUnifiedInt64(writer, value.OverlappedValueInt64);
+ break;
+ case RedisValue.StorageType.UInt64:
+ WriteUnifiedUInt64(writer, value.OverlappedValueUInt64);
+ break;
+ case RedisValue.StorageType.Double:
+ WriteUnifiedDouble(writer, value.OverlappedValueDouble);
+ break;
+ case RedisValue.StorageType.String:
+ WriteUnifiedPrefixedString(writer, null, value.RawString());
+ break;
+ case RedisValue.StorageType.MemoryManager or RedisValue.StorageType.ByteArray:
+ WriteUnifiedSpan(writer, value.RawSpan());
+ break;
+ default:
+ throw new InvalidOperationException($"Unexpected {value.Type} value: '{value}'");
+ }
+ }
+
+ internal void WriteBulkString(ReadOnlySpan value) => 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(string command, int arguments)
+ {
+ byte[]? lease = null;
+ try
+ {
+ int bytes = Encoding.ASCII.GetMaxByteCount(command.Length);
+ Span buffer = command.Length <= 32 ? stackalloc byte[32] : (lease = ArrayPool.Shared.Rent(bytes));
+ bytes = Encoding.ASCII.GetBytes(command, buffer);
+ var span = buffer.Slice(0, bytes);
+ AsciiHash.ToUpper(span);
+ WriteHeader(RedisCommand.UNKNOWN, arguments, span);
+ }
+ finally
+ {
+ if (lease is not null) ArrayPool.Shared.Return(lease);
+ }
+ }
+
+ internal void WriteHeader(RedisCommand command, int arguments) => WriteHeader(command, arguments, _map.GetBytes(command).Span);
+
+ internal void WriteHeader(RedisCommand command, int arguments, ReadOnlySpan commandBytes)
+ {
+ // using >= here because we will be adding 1 for the command itself (which is an arg for the purposes of the multi-bulk protocol)
+ if (arguments >= REDIS_MAX_ARGS) throw ExceptionFactory.TooManyArgs(command.ToString(), arguments);
+
+ // in theory we should never see this; CheckMessage dealt with "regular" messages, and
+ // ExecuteMessage should have dealt with everything else
+ if (commandBytes.IsEmpty) throw ExceptionFactory.CommandDisabled(command);
+
+ // *{argCount}\r\n = 3 + MaxInt32TextLen
+ // ${cmd-len}\r\n = 3 + MaxInt32TextLen
+ // {cmd}\r\n = 2 + commandBytes.Length
+ var span = _writer.GetSpan(commandBytes.Length + 8 + Format.MaxInt32TextLen + Format.MaxInt32TextLen);
+ span[0] = (byte)'*';
+
+ int offset = WriteRaw(span, arguments + 1, offset: 1);
+ span[offset++] = (byte)'$';
+ offset = AppendToSpan(span, commandBytes, offset: offset);
+
+ _writer.Advance(offset);
+ }
+
+ internal static void WriteMultiBulkHeader(IBufferWriter writer, long count)
+ {
+ // *{count}\r\n = 3 + MaxInt32TextLen
+ var span = writer.GetSpan(3 + Format.MaxInt32TextLen);
+ span[0] = (byte)'*';
+ int offset = WriteRaw(span, count, offset: 1);
+ writer.Advance(offset);
+ }
+
+ internal static void WriteMultiBulkHeader(IBufferWriter writer, long count, RespPrefix prefix)
+ {
+ // *{count}\r\n = 3 + MaxInt32TextLen
+ var span = writer.GetSpan(3 + Format.MaxInt32TextLen);
+ span[0] = (byte)prefix;
+ if ((prefix is RespPrefix.Map or RespPrefix.Attribute) & count > 0)
+ {
+ if ((count & 1) != 0) Throw(prefix, count);
+ count >>= 1;
+ static void Throw(RespPrefix 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);
+ writer.Advance(offset);
+ }
+
+ private static ReadOnlySpan NullBulkString => "$-1\r\n"u8;
+ private static ReadOnlySpan EmptyBulkString => "$0\r\n\r\n"u8;
+
+ internal static void WriteUnifiedPrefixedString(IBufferWriter writer, byte[]? prefix, string? value)
+ {
+ if (value == null)
+ {
+ // special case
+ writer.Write(NullBulkString);
+ }
+ else
+ {
+ // ${total-len}\r\n 3 + MaxInt32TextLen
+ // {prefix}{value}\r\n
+ int encodedLength = Encoding.UTF8.GetByteCount(value),
+ prefixLength = prefix?.Length ?? 0,
+ totalLength = prefixLength + encodedLength;
+
+ if (totalLength == 0)
+ {
+ // special-case
+ writer.Write(EmptyBulkString);
+ }
+ else
+ {
+ var span = writer.GetSpan(3 + Format.MaxInt32TextLen);
+ span[0] = (byte)'$';
+ int bytes = WriteRaw(span, totalLength, offset: 1);
+ writer.Advance(bytes);
+
+ if (prefixLength != 0) writer.Write(prefix);
+ if (encodedLength != 0) WriteRaw(writer, value, encodedLength);
+ WriteCrlf(writer);
+ }
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static int WriteCrlf(Span span, int offset)
+ {
+ span[offset++] = (byte)'\r';
+ span[offset++] = (byte)'\n';
+ return offset;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static void WriteCrlf(IBufferWriter writer)
+ {
+ var span = writer.GetSpan(2);
+ span[0] = (byte)'\r';
+ span[1] = (byte)'\n';
+ writer.Advance(2);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal void WriteRaw(ReadOnlySpan value) => _writer.Write(value);
+
+ internal static int WriteRaw(Span span, long value, bool withLengthPrefix = false, int offset = 0)
+ {
+ if (value >= 0 && value <= 9)
+ {
+ if (withLengthPrefix)
+ {
+ span[offset++] = (byte)'1';
+ offset = WriteCrlf(span, offset);
+ }
+
+ span[offset++] = (byte)((int)'0' + (int)value);
+ }
+ else if (value >= 10 && value < 100)
+ {
+ if (withLengthPrefix)
+ {
+ span[offset++] = (byte)'2';
+ offset = WriteCrlf(span, offset);
+ }
+
+ span[offset++] = (byte)((int)'0' + ((int)value / 10));
+ span[offset++] = (byte)((int)'0' + ((int)value % 10));
+ }
+ else if (value >= 100 && value < 1000)
+ {
+ int v = (int)value;
+ int units = v % 10;
+ v /= 10;
+ int tens = v % 10, hundreds = v / 10;
+ if (withLengthPrefix)
+ {
+ span[offset++] = (byte)'3';
+ offset = WriteCrlf(span, offset);
+ }
+
+ span[offset++] = (byte)((int)'0' + hundreds);
+ span[offset++] = (byte)((int)'0' + tens);
+ span[offset++] = (byte)((int)'0' + units);
+ }
+ else if (value < 0 && value >= -9)
+ {
+ if (withLengthPrefix)
+ {
+ span[offset++] = (byte)'2';
+ offset = WriteCrlf(span, offset);
+ }
+
+ span[offset++] = (byte)'-';
+ span[offset++] = (byte)((int)'0' - (int)value);
+ }
+ else if (value <= -10 && value > -100)
+ {
+ if (withLengthPrefix)
+ {
+ span[offset++] = (byte)'3';
+ offset = WriteCrlf(span, offset);
+ }
+
+ value = -value;
+ span[offset++] = (byte)'-';
+ span[offset++] = (byte)((int)'0' + ((int)value / 10));
+ span[offset++] = (byte)((int)'0' + ((int)value % 10));
+ }
+ else
+ {
+ // we're going to write it, but *to the wrong place*
+ var availableChunk = span.Slice(offset);
+ var formattedLength = Format.FormatInt64(value, availableChunk);
+ if (withLengthPrefix)
+ {
+ // now we know how large the prefix is: write the prefix, then write the value
+ var prefixLength = Format.FormatInt32(formattedLength, availableChunk);
+ offset += prefixLength;
+ offset = WriteCrlf(span, offset);
+
+ availableChunk = span.Slice(offset);
+ var finalLength = Format.FormatInt64(value, availableChunk);
+ offset += finalLength;
+ Debug.Assert(finalLength == formattedLength);
+ }
+ else
+ {
+ offset += formattedLength;
+ }
+ }
+
+ return WriteCrlf(span, offset);
+ }
+
+ [ThreadStatic]
+ private static Encoder? s_PerThreadEncoder;
+
+ internal static Encoder GetPerThreadEncoder()
+ {
+ var encoder = s_PerThreadEncoder;
+ if (encoder == null)
+ {
+ s_PerThreadEncoder = encoder = Encoding.UTF8.GetEncoder();
+ }
+ else
+ {
+ encoder.Reset();
+ }
+
+ return encoder;
+ }
+
+ internal static unsafe void WriteRaw(IBufferWriter writer, string value, int expectedLength)
+ {
+ const int MaxQuickEncodeSize = 512;
+
+ fixed (char* cPtr = value)
+ {
+ int totalBytes;
+ if (expectedLength <= MaxQuickEncodeSize)
+ {
+ // encode directly in one hit
+ var span = writer.GetSpan(expectedLength);
+ fixed (byte* bPtr = span)
+ {
+ totalBytes = Encoding.UTF8.GetBytes(
+ cPtr,
+ value.Length,
+ bPtr,
+ expectedLength);
+ }
+
+ writer.Advance(expectedLength);
+ }
+ else
+ {
+ // use an encoder in a loop
+ var encoder = GetPerThreadEncoder();
+ int charsRemaining = value.Length, charOffset = 0;
+ totalBytes = 0;
+
+ bool final = false;
+ while (true)
+ {
+ var span = writer
+ .GetSpan(5); // get *some* memory - at least enough for 1 character (but hopefully lots more)
+
+ int charsUsed, bytesUsed;
+ bool completed;
+ fixed (byte* bPtr = span)
+ {
+ encoder.Convert(
+ cPtr + charOffset,
+ charsRemaining,
+ bPtr,
+ span.Length,
+ final,
+ out charsUsed,
+ out bytesUsed,
+ out completed);
+ }
+
+ writer.Advance(bytesUsed);
+ totalBytes += bytesUsed;
+ charOffset += charsUsed;
+ charsRemaining -= charsUsed;
+
+ if (charsRemaining <= 0)
+ {
+ if (charsRemaining < 0) throw new InvalidOperationException("String encode went negative");
+ if (completed) break; // fine
+ if (final) throw new InvalidOperationException("String encode failed to complete");
+ final = true; // flush the encoder to one more span, then exit
+ }
+ }
+ }
+
+ if (totalBytes != expectedLength) throw new InvalidOperationException("String encode length check failure");
+ }
+ }
+
+ private static void WriteUnifiedPrefixedBlob(IBufferWriter writer, byte[]? prefix, byte[]? value)
+ {
+ // ${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;
+ // even if prefixed, a null value writes as null, so can use the non-prefixed version
+ WriteUnifiedBlob(writer, value);
+ }
+ else
+ {
+ 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);
+
+ writer.Write(prefix);
+ writer.Write(value);
+
+ span = writer.GetSpan(2);
+ WriteCrlf(span, 0);
+ writer.Advance(2);
+ }
+ }
+
+ 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"
+
+ // ${asc-len}\r\n = 4/5 (asc-len at most 2 digits)
+ // {asc}\r\n = MaxInt64TextLen + 2
+ var span = writer.GetSpan(7 + Format.MaxInt64TextLen);
+
+ span[0] = (byte)'$';
+ var bytes = WriteRaw(span, value, withLengthPrefix: true, offset: 1);
+ writer.Advance(bytes);
+ }
+
+ 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"
+ 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(IBufferWriter 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(IBufferWriter writer, long value)
+ {
+ // note: client should never write integer; only server does this
+ // :{asc}\r\n = MaxInt64TextLen + 3
+ var span = writer.GetSpan(3 + Format.MaxInt64TextLen);
+
+ span[0] = (byte)':';
+ var bytes = WriteRaw(span, value, withLengthPrefix: false, offset: 1);
+ writer.Advance(bytes);
+ }
+
+ private static void WriteUnifiedBlob(IBufferWriter writer, byte[]? value)
+ {
+ if (value is null)
+ {
+ // special case:
+ writer.Write(NullBulkString);
+ }
+ else
+ {
+ WriteUnifiedSpan(writer, new ReadOnlySpan(value));
+ }
+ }
+
+ private static void WriteUnifiedSpan(IBufferWriter writer, ReadOnlySpan value)
+ {
+ // ${len}\r\n = 3 + MaxInt32TextLen
+ // {value}\r\n = 2 + value.Length
+ const int MaxQuickSpanSize = 512;
+ if (value.Length == 0)
+ {
+ // special case:
+ writer.Write(EmptyBulkString);
+ }
+ else if (value.Length <= MaxQuickSpanSize)
+ {
+ var span = writer.GetSpan(5 + Format.MaxInt32TextLen + value.Length);
+ span[0] = (byte)'$';
+ int bytes = AppendToSpan(span, value, 1);
+ writer.Advance(bytes);
+ }
+ else
+ {
+ // too big to guarantee can do in a single span
+ var span = writer.GetSpan(3 + Format.MaxInt32TextLen);
+ span[0] = (byte)'$';
+ int bytes = WriteRaw(span, value.Length, offset: 1);
+ writer.Advance(bytes);
+
+ writer.Write(value);
+
+ WriteCrlf(writer);
+ }
+ }
+
+ private static int AppendToSpan(Span span, ReadOnlySpan value, int offset = 0)
+ {
+ offset = WriteRaw(span, value.Length, offset: offset);
+ value.CopyTo(span.Slice(offset, value.Length));
+ offset += value.Length;
+ return WriteCrlf(span, offset);
+ }
+
+ internal void WriteSha1AsHex(byte[]? value)
+ {
+ var writer = _writer;
+ if (value is null)
+ {
+ writer.Write(NullBulkString);
+ }
+ else if (value.Length == ResultProcessor.ScriptLoadProcessor.Sha1HashLength)
+ {
+ // $40\r\n = 5
+ // {40 bytes}\r\n = 42
+ var span = writer.GetSpan(47);
+ span[0] = (byte)'$';
+ span[1] = (byte)'4';
+ span[2] = (byte)'0';
+ span[3] = (byte)'\r';
+ span[4] = (byte)'\n';
+
+ int offset = 5;
+ for (int i = 0; i < value.Length; i++)
+ {
+ var b = value[i];
+ span[offset++] = ToHexNibble(b >> 4);
+ span[offset++] = ToHexNibble(b & 15);
+ }
+
+ span[offset++] = (byte)'\r';
+ span[offset++] = (byte)'\n';
+
+ writer.Advance(offset);
+ }
+ else
+ {
+ throw new InvalidOperationException("Invalid SHA1 length: " + value.Length);
+ }
+ }
+
+ internal static byte ToHexNibble(int value)
+ {
+ return value < 10 ? (byte)('0' + value) : (byte)('a' - 10 + value);
+ }
+}
diff --git a/src/StackExchange.Redis/NullableHacks.cs b/src/StackExchange.Redis/NullableHacks.cs
deleted file mode 100644
index 4ebebf73b..000000000
--- a/src/StackExchange.Redis/NullableHacks.cs
+++ /dev/null
@@ -1,148 +0,0 @@
-// 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; }
- }
-#endif
-
-#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
- {
- /// 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/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs
index 36d8268bf..230083883 100644
--- a/src/StackExchange.Redis/PhysicalBridge.cs
+++ b/src/StackExchange.Redis/PhysicalBridge.cs
@@ -854,9 +854,9 @@ internal WriteResult WriteMessageTakingWriteLockSync(PhysicalConnection physical
var result = WriteMessageInsideLock(physical, message);
- if (result == WriteResult.Success)
+ if (result == WriteResult.Success & message.IsFlushRequiredSync)
{
- result = physical.FlushSync(false, TimeoutMilliseconds);
+ physical.Flush();
}
physical.SetIdle();
@@ -1179,7 +1179,8 @@ private void ProcessBridgeBacklog()
// 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)
+ bool flush = false;
+ while (IsConnected && physical is { HasOutputPipe: true })
{
Message? message;
_backlogStatus = BacklogStatus.CheckingForWork;
@@ -1199,13 +1200,8 @@ private void ProcessBridgeBacklog()
_backlogStatus = BacklogStatus.WritingMessage;
var result = WriteMessageInsideLock(physical, message);
- if (result == WriteResult.Success)
- {
- _backlogStatus = BacklogStatus.Flushing;
-#pragma warning disable CS0618 // Type or member is obsolete
- result = physical.FlushSync(false, TimeoutMilliseconds);
-#pragma warning restore CS0618 // Type or member is obsolete
- }
+ // use flush logic that assumes a sync caller
+ if (!flush) flush = message.IsFlushRequiredSync;
_backlogStatus = BacklogStatus.MarkingInactive;
if (result != WriteResult.Success)
@@ -1225,6 +1221,14 @@ private void ProcessBridgeBacklog()
UnmarkActiveMessage(message);
}
}
+
+ if (flush && IsConnected && physical is { HasOutputPipe: true })
+ {
+ // at least one message wants flushing
+ _backlogStatus = BacklogStatus.Flushing;
+ physical?.Flush();
+ }
+
_backlogStatus = BacklogStatus.SettingIdle;
physical?.SetIdle();
_backlogStatus = BacklogStatus.Inactive;
@@ -1310,7 +1314,7 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect
// an actual timeout
#if NET
var pending = _singleWriterMutex.WaitAsync(TimeoutMilliseconds);
- if (pending.Status != TaskStatus.RanToCompletion) return WriteMessageTakingWriteLockAsync_Awaited(pending, physical, message);
+ if (pending.IsCompletedSuccessfully) return WriteMessageTakingWriteLockAsync_Awaited(pending, physical, message);
gotLock = pending.Result; // fine since we know we got a result
if (!gotLock) return new ValueTask(TimedOutBeforeWrite(message));
@@ -1323,20 +1327,9 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect
#endif
}
var result = WriteMessageInsideLock(physical, message);
- if (result == WriteResult.Success)
+ if (result == WriteResult.Success & message.IsFlushRequiredAsync)
{
- var flush = physical.FlushAsync(false);
- if (!flush.IsCompletedSuccessfully)
- {
- releaseLock = false; // so we don't release prematurely
-#if NET
- return CompleteWriteAndReleaseLockAsync(flush, message);
-#else
- return CompleteWriteAndReleaseLockAsync(token, flush, message);
-#endif
- }
-
- result = flush.Result; // .Result: we know it was completed, so this is fine
+ physical.Flush();
}
physical.SetIdle();
@@ -1394,7 +1387,7 @@ private async ValueTask WriteMessageTakingWriteLockAsync_Awaited(
if (result == WriteResult.Success)
{
- result = await physical.FlushAsync(false).ForAwait();
+ physical.Flush();
}
physical.SetIdle();
@@ -1495,7 +1488,7 @@ private bool ChangeState(State oldState, State newState)
Interlocked.Exchange(ref connectStartTicks, Environment.TickCount);
// separate creation and connection for case when connection completes synchronously
// in that case PhysicalConnection will call back to PhysicalBridge, and most PhysicalBridge methods assume that physical is not null;
- physical = new PhysicalConnection(this);
+ physical = new PhysicalConnection(this, Multiplexer.RawConfig.WriteMode);
physical.BeginConnectAsync(log).RedisFireAndForget();
}
@@ -1715,6 +1708,8 @@ private uint NextHighIntegrityTokenInsideLock()
}
}
+ internal bool CanSimulateConnectionFailure => Multiplexer.RawConfig.AllowAdmin && physical?.CanSimulateConnectionFailure == true;
+
///
/// For testing only.
///
diff --git a/src/StackExchange.Redis/PhysicalConnection.Read.cs b/src/StackExchange.Redis/PhysicalConnection.Read.cs
new file mode 100644
index 000000000..a7870498d
--- /dev/null
+++ b/src/StackExchange.Redis/PhysicalConnection.Read.cs
@@ -0,0 +1,790 @@
+using System;
+using System.Buffers;
+using System.Buffers.Binary;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using RESPite;
+using RESPite.Buffers;
+using RESPite.Internal;
+using RESPite.Messages;
+
+namespace StackExchange.Redis;
+
+internal sealed partial class PhysicalConnection
+{
+ private long totalBytesReceived;
+
+ internal static PhysicalConnection Dummy(BufferedStreamWriter.WriteMode writeMode = BufferedStreamWriter.WriteMode.Default)
+ => new(null!, writeMode);
+
+ private volatile ReadStatus _readStatus = ReadStatus.NotStarted;
+ internal ReadStatus GetReadStatus() => _readStatus;
+
+ private BufferedStreamWriter.WriteMode WriteMode { get; }
+
+ internal void StartReading(CancellationToken cancellation = default)
+ {
+ if (cancellation.CanBeCanceled && cancellation != InputCancel)
+ {
+ cancellation.ThrowIfCancellationRequested();
+ if (InputCancel.CanBeCanceled)
+ {
+ cancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellation, InputCancel).Token;
+ }
+ }
+ else
+ {
+ cancellation = InputCancel;
+ }
+
+ if (_output is { IsSync: true })
+ {
+ StartReadingSync(this, cancellation);
+ static void StartReadingSync(PhysicalConnection conn, CancellationToken cancellation)
+ {
+ // this method exists purely to limit capture context scope
+ Thread thread = new Thread(() => conn.ReadAllSync(cancellation))
+ {
+ IsBackground = true,
+ Priority = ThreadPriority.AboveNormal,
+ Name = "SE.Redis Sync Writer",
+ };
+ thread.Start();
+ }
+ }
+ else
+ {
+ ReadAllAsync(cancellation).RedisFireAndForget();
+ }
+ }
+
+ private async Task ReadAllAsync(CancellationToken cancellationToken)
+ {
+ var tail = _ioStream ?? Stream.Null;
+ _readStatus = ReadStatus.Init;
+ _readState = default;
+ _readBuffer = CycleBuffer.Create();
+ try
+ {
+ int read;
+ do
+ {
+ _readStatus = ReadStatus.ReadAsync;
+ var buffer = _readBuffer.GetUncommittedMemory();
+ var pending = tail.ReadAsync(buffer, cancellationToken);
+#if DEBUG
+ bool inline = pending.IsCompleted;
+#endif
+ read = await pending.ConfigureAwait(false);
+ _readStatus = ReadStatus.UpdateWriteTime;
+ UpdateLastReadTime();
+#if DEBUG
+ DebugCounters.OnAsyncRead(read, inline);
+#endif
+ _readStatus = ReadStatus.TryParseResult;
+ }
+ // another formatter glitch
+ while (CommitAndParseFrames(read) && !ForceReconnect);
+
+ _readStatus = ReadStatus.ProcessBufferComplete;
+
+ // Volatile.Write(ref _readStatus, ReaderCompleted);
+ _readBuffer.Release(); // clean exit, we can recycle
+ _readStatus = ReadStatus.RanToCompletion;
+ RecordConnectionFailed(ConnectionFailureType.SocketClosed);
+ }
+ catch (EndOfStreamException) when (_readStatus is ReadStatus.ReadAsync)
+ {
+ _readStatus = ReadStatus.RanToCompletion;
+ RecordConnectionFailed(ConnectionFailureType.SocketClosed);
+ }
+ catch (OperationCanceledException) when (_readStatus is ReadStatus.ReadAsync)
+ {
+ _readStatus = ReadStatus.RanToCompletion;
+ RecordConnectionFailed(ConnectionFailureType.SocketClosed);
+ }
+ catch (Exception ex)
+ {
+ _readStatus = ReadStatus.Faulted;
+ RecordConnectionFailed(ConnectionFailureType.InternalFailure, ex);
+ }
+ finally
+ {
+ _readBuffer = default; // wipe, however we exited
+ }
+ }
+
+ private void ReadAllSync(CancellationToken cancellationToken)
+ {
+ var tail = _ioStream ?? Stream.Null;
+ _readStatus = ReadStatus.Init;
+ _readState = default;
+ _readBuffer = CycleBuffer.Create();
+ try
+ {
+ int read;
+ do
+ {
+ _readStatus = ReadStatus.ReadAsync;
+ var buffer = _readBuffer.GetUncommittedMemory();
+ cancellationToken.ThrowIfCancellationRequested();
+#if NET || NETSTANDARD2_1_OR_GREATER
+ read = tail.Read(buffer.Span);
+#else
+ read = tail.Read(buffer);
+#endif
+
+ _readStatus = ReadStatus.UpdateWriteTime;
+ UpdateLastReadTime();
+
+ DebugCounters.OnSyncRead(read);
+ _readStatus = ReadStatus.TryParseResult;
+ }
+ // another formatter glitch
+ while (CommitAndParseFrames(read) && !ForceReconnect);
+
+ _readStatus = ReadStatus.ProcessBufferComplete;
+
+ // Volatile.Write(ref _readStatus, ReaderCompleted);
+ _readBuffer.Release(); // clean exit, we can recycle
+ _readStatus = ReadStatus.RanToCompletion;
+ RecordConnectionFailed(ConnectionFailureType.SocketClosed);
+ }
+ catch (EndOfStreamException) when (_readStatus is ReadStatus.ReadAsync)
+ {
+ _readStatus = ReadStatus.RanToCompletion;
+ RecordConnectionFailed(ConnectionFailureType.SocketClosed);
+ }
+ catch (OperationCanceledException) when (_readStatus is ReadStatus.ReadAsync)
+ {
+ _readStatus = ReadStatus.RanToCompletion;
+ RecordConnectionFailed(ConnectionFailureType.SocketClosed);
+ }
+ catch (Exception ex)
+ {
+ _readStatus = ReadStatus.Faulted;
+ RecordConnectionFailed(ConnectionFailureType.InternalFailure, ex);
+ }
+ finally
+ {
+ _readBuffer = default; // wipe, however we exited
+ }
+ }
+
+ private bool ForceReconnect => BridgeCouldBeNull?.NeedsReconnect == true;
+
+ private static byte[]? SharedNoLease;
+
+ private CycleBuffer _readBuffer;
+ private RespScanState _readState = default;
+
+ private long GetReadCommittedLength()
+ {
+ try
+ {
+ var len = _readBuffer.GetCommittedLength();
+ return len < 0 ? -1 : len;
+ }
+ catch
+ {
+ return -1;
+ }
+ }
+
+ private bool CommitAndParseFrames(int bytesRead)
+ {
+ if (bytesRead <= 0)
+ {
+ return false;
+ }
+ ref RespScanState state = ref _readState; // avoid a ton of ldarg0
+
+ totalBytesReceived += bytesRead;
+#if PARSE_DETAIL
+ string src = $"parse {bytesRead}";
+ try
+#endif
+ {
+ Debug.Assert(_readBuffer.GetCommittedLength() >= 0, "multi-segment running-indices are corrupt");
+#if PARSE_DETAIL
+ src += $" ({_readBuffer.GetCommittedLength()}+{bytesRead}-{state.TotalBytes})";
+#endif
+ Debug.Assert(
+ bytesRead <= _readBuffer.UncommittedAvailable,
+ $"Insufficient bytes in {nameof(CommitAndParseFrames)}; got {bytesRead}, Available={_readBuffer.UncommittedAvailable}");
+ _readBuffer.Commit(bytesRead);
+#if PARSE_DETAIL
+ src += $",total {_readBuffer.GetCommittedLength()}";
+#endif
+ var scanner = RespFrameScanner.Default;
+
+ OperationStatus status = OperationStatus.NeedMoreData;
+ if (_readBuffer.TryGetCommitted(out var fullSpan))
+ {
+ int fullyConsumed = 0;
+ var toParse = fullSpan.Slice((int)state.TotalBytes); // skip what we've already parsed
+ OnDetailLog($"parsing {toParse.Length} bytes, single buffer");
+
+ Debug.Assert(!toParse.IsEmpty);
+ while (true)
+ {
+#if PARSE_DETAIL
+ src += $",span {toParse.Length}";
+#endif
+ int totalBytesBefore = (int)state.TotalBytes;
+ if (toParse.Length < RespScanState.MinBytes
+ || (status = scanner.TryRead(ref state, toParse)) != OperationStatus.Done)
+ {
+ break;
+ }
+
+ Debug.Assert(
+ state is
+ {
+ IsComplete: true, TotalBytes: >= RespScanState.MinBytes, Prefix: not RespPrefix.None
+ },
+ "Invalid RESP read state");
+
+ // extract the frame
+ var bytes = (int)state.TotalBytes;
+#if PARSE_DETAIL
+ src += $",frame {bytes}";
+#endif
+ // send the frame somewhere (note this is the *full* frame, not just the bit we just parsed)
+ OnDetailLog($"found {state.Prefix} frame, {bytes} bytes");
+ OnResponseFrame(state.Prefix, fullSpan.Slice(fullyConsumed, bytes), ref SharedNoLease);
+ UpdateBufferStats(bytes, toParse.Length);
+
+ // update our buffers to the unread potions and reset for a new RESP frame
+ fullyConsumed += bytes;
+ toParse = toParse.Slice(bytes - totalBytesBefore); // move past the extra bytes we just read
+ state = default;
+ status = OperationStatus.NeedMoreData;
+ }
+ OnDetailLog($"discarding {fullyConsumed} bytes");
+ _readBuffer.DiscardCommitted(fullyConsumed);
+ }
+ else // the same thing again, but this time with multi-segment sequence
+ {
+ var fullSequence = _readBuffer.GetAllCommitted();
+ Debug.Assert(
+ fullSequence is { IsEmpty: false, IsSingleSegment: false },
+ "non-trivial sequence expected");
+
+ long fullyConsumed = 0;
+ var toParse = fullSequence.Slice((int)state.TotalBytes); // skip what we've already parsed
+ OnDetailLog($"parsing {toParse.Length} bytes, multi-buffer");
+
+ while (true)
+ {
+#if PARSE_DETAIL
+ src += $",ros {toParse.Length}";
+#endif
+ int totalBytesBefore = (int)state.TotalBytes;
+ if (toParse.Length < RespScanState.MinBytes
+ || (status = scanner.TryRead(ref state, toParse)) != OperationStatus.Done)
+ {
+ break;
+ }
+
+ Debug.Assert(
+ state is
+ {
+ IsComplete: true, TotalBytes: >= RespScanState.MinBytes, Prefix: not RespPrefix.None
+ },
+ "Invalid RESP read state");
+
+ // extract the frame
+ var bytes = (int)state.TotalBytes;
+#if PARSE_DETAIL
+ src += $",frame {bytes}";
+#endif
+ // send the frame somewhere (note this is the *full* frame, not just the bit we just parsed)
+ OnDetailLog($"found {state.Prefix} frame, {bytes} bytes");
+ OnResponseFrame(state.Prefix, fullSequence.Slice(fullyConsumed, bytes));
+ UpdateBufferStats(bytes, toParse.Length);
+
+ // update our buffers to the unread potions and reset for a new RESP frame
+ fullyConsumed += bytes;
+ toParse = toParse.Slice(bytes - totalBytesBefore); // move past the extra bytes we just read
+ state = default;
+ status = OperationStatus.NeedMoreData;
+ }
+
+ OnDetailLog($"discarding {fullyConsumed} bytes");
+ _readBuffer.DiscardCommitted(fullyConsumed);
+ }
+
+ if (status != OperationStatus.NeedMoreData)
+ {
+ ThrowStatus(status);
+
+ static void ThrowStatus(OperationStatus status) =>
+ throw new InvalidOperationException($"Unexpected operation status: {status}");
+ }
+
+ return true;
+ }
+#if PARSE_DETAIL
+ catch (Exception ex)
+ {
+ OnDetailLog($"{nameof(CommitAndParseFrames)}: {ex.Message}");
+ OnDetailLog(src);
+ if (Debugger.IsAttached) Debugger.Break();
+ throw new InvalidOperationException($"{src} lead to {ex.Message}", ex);
+ }
+#endif
+ }
+
+ [Conditional("PARSE_DETAIL")]
+ internal void OnDetailLog(string message)
+ {
+#if PARSE_DETAIL
+ message = $"[{id}] {message}";
+ lock (LogLock)
+ {
+ Console.WriteLine(message);
+ Debug.WriteLine(message);
+ File.AppendAllText("verbose.log", message + Environment.NewLine);
+ }
+#endif
+ }
+
+#if PARSE_DETAIL
+ private static int s_id;
+ private readonly int id = Interlocked.Increment(ref s_id);
+ private static readonly object LogLock = new();
+#endif
+
+ private void OnResponseFrame(RespPrefix prefix, ReadOnlySequence payload)
+ {
+ if (payload.IsSingleSegment)
+ {
+#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER
+ OnResponseFrame(prefix, payload.FirstSpan, ref SharedNoLease);
+#else
+ OnResponseFrame(prefix, payload.First.Span, ref SharedNoLease);
+#endif
+ }
+ else
+ {
+ var len = checked((int)payload.Length);
+ byte[]? oversized = ArrayPool.Shared.Rent(len);
+ payload.CopyTo(oversized);
+ OnResponseFrame(prefix, new(oversized, 0, len), ref oversized);
+
+ // the lease could have been claimed by the activation code (to prevent another memcpy); otherwise, free
+ if (oversized is not null)
+ {
+ ArrayPool.Shared.Return(oversized);
+ }
+ }
+ }
+
+ private void UpdateBufferStats(int lastResult, long inBuffer)
+ {
+ // Track the last result size *after* processing for the *next* error message
+ bytesInBuffer = inBuffer;
+ bytesLastResult = lastResult;
+ }
+
+ private void OnResponseFrame(RespPrefix prefix, ReadOnlySpan frame, ref byte[]? lease)
+ {
+ DebugValidateSingleFrame(frame);
+ _readStatus = ReadStatus.MatchResult;
+
+ switch (prefix)
+ {
+ case RespPrefix.Push: // explicit RESP3 push message
+ case RespPrefix.Array when (_protocol is RedisProtocol.Resp2 & connectionType is ConnectionType.Subscription)
+ && !IsArrayPong(frame): // could be a RESP2 pub/sub payload
+ // out-of-band; pub/sub etc
+ if (OnOutOfBand(frame, ref lease))
+ {
+ OnDetailLog($"out-of-band message, not dequeuing: {prefix}");
+ return;
+ }
+ break;
+ }
+
+ // request/response; match to inbound
+ MatchNextResult(frame);
+
+ static bool IsArrayPong(ReadOnlySpan payload)
+ {
+ if (payload.Length >= sizeof(ulong))
+ {
+ var hash = AsciiHash.HashCS(payload);
+ switch (hash)
+ {
+ case ArrayPong_LC_Bulk.HashCS when payload.StartsWith(ArrayPong_LC_Bulk.U8):
+ case ArrayPong_UC_Bulk.HashCS when payload.StartsWith(ArrayPong_UC_Bulk.U8):
+ case ArrayPong_LC_Simple.HashCS when payload.StartsWith(ArrayPong_LC_Simple.U8):
+ case ArrayPong_UC_Simple.HashCS when payload.StartsWith(ArrayPong_UC_Simple.U8):
+ var reader = new RespReader(payload);
+ return reader.SafeTryMoveNext() // have root
+ && reader.Prefix == RespPrefix.Array // root is array
+ && reader.SafeTryMoveNext() // have first child
+ && (reader.IsInlneCpuUInt32(pong) || reader.IsInlneCpuUInt32(PONG)); // pong
+ }
+ }
+
+ return false;
+ }
+ }
+
+ 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);
+ }
+
+ internal static ReadOnlySpan StackCopyLengthChecked(scoped in RespReader reader, Span buffer)
+ {
+ var len = reader.CopyTo(buffer);
+ if (len == buffer.Length && reader.ScalarLength() > len) return default; // too small
+ return buffer.Slice(0, len);
+ }
+
+ private bool OnOutOfBand(ReadOnlySpan payload, ref byte[]? lease)
+ {
+ var muxer = BridgeCouldBeNull?.Multiplexer;
+ if (muxer is null) return true; // consume it blindly
+
+ var reader = new RespReader(payload);
+
+ // read the message kind from the first element
+ if (reader.SafeTryMoveNext() & reader.IsAggregate & !reader.IsStreaming
+ && reader.AggregateLength() >= 2
+ && (reader.SafeTryMoveNext() & reader.IsInlineScalar & !reader.IsError))
+ {
+ PushKind kind;
+ unsafe
+ {
+ if (!reader.TryParseScalar(&PushKindMetadata.TryParse, out kind)) kind = PushKind.None;
+ }
+
+ RedisChannel.RedisChannelOptions channelOptions = kind switch
+ {
+ PushKind.PMessage or PushKind.PSubscribe or PushKind.PUnsubscribe => RedisChannel.RedisChannelOptions.Pattern,
+ PushKind.SMessage or PushKind.SSubscribe or PushKind.SUnsubscribe => RedisChannel.RedisChannelOptions.Sharded,
+ _ => RedisChannel.RedisChannelOptions.None,
+ };
+
+ static bool TryMoveNextString(ref RespReader reader)
+ => reader.SafeTryMoveNext() & reader.IsInlineScalar &
+ reader.Prefix is RespPrefix.BulkString or RespPrefix.SimpleString;
+
+ if (kind is PushKind.None || !TryMoveNextString(ref reader)) return false;
+
+ // the channel is always the second element
+ var subscriptionChannel = AsRedisChannel(reader, channelOptions);
+
+ switch (kind)
+ {
+ case (PushKind.Message or PushKind.SMessage) when reader.SafeTryMoveNext():
+ _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 && reader.Prefix is RespPrefix.BulkString or RespPrefix.SimpleString && subscriptionChannel.Span.SequenceEqual(configChanged))
+ {
+ EndPoint? blame = null;
+ try
+ {
+ if (!reader.Is("*"u8))
+ {
+ // We don't want to fail here, just trying to identify
+ _ = Format.TryParseEndPoint(reader.ReadString(), out blame);
+ }
+ }
+ catch
+ {
+ /* no biggie */
+ }
+
+ Trace("Configuration changed: " + Format.ToString(blame));
+ _readStatus = ReadStatus.Reconfigure;
+ muxer.ReconfigureIfNeeded(blame, true, "broadcast");
+ }
+
+ // invoke the handlers
+ if (!subscriptionChannel.IsNull)
+ {
+ Trace($"{kind}: {subscriptionChannel}");
+ OnMessage(muxer, subscriptionChannel, subscriptionChannel, in reader);
+ }
+
+ return true;
+ case PushKind.PMessage when TryMoveNextString(ref reader):
+ _readStatus = ReadStatus.PubSubPMessage;
+
+ var messageChannel = AsRedisChannel(reader, RedisChannel.RedisChannelOptions.None);
+ if (!messageChannel.IsNull && reader.SafeTryMoveNext())
+ {
+ Trace($"{kind}: {messageChannel} via {subscriptionChannel}");
+ OnMessage(muxer, subscriptionChannel, messageChannel, in reader);
+ }
+
+ return true;
+ 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))
+ {
+ // 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: "sunsubscribe");
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void OnMessage(
+ ConnectionMultiplexer muxer,
+ in RedisChannel subscriptionChannel,
+ in RedisChannel messageChannel,
+ in RespReader reader)
+ {
+ // note: this could be multi-message: https://github.com/StackExchange/StackExchange.Redis/issues/2507
+ _readStatus = ReadStatus.InvokePubSub;
+ switch (reader.Prefix)
+ {
+ case RespPrefix.BulkString:
+ case RespPrefix.SimpleString:
+ muxer.OnMessage(subscriptionChannel, messageChannel, reader.ReadRedisValue());
+ break;
+ case RespPrefix.Array:
+ var iter = reader.AggregateChildren();
+ while (iter.MoveNext())
+ {
+ muxer.OnMessage(subscriptionChannel, messageChannel, iter.Value.ReadRedisValue());
+ }
+
+ break;
+ }
+ }
+
+ private void MatchNextResult(ReadOnlySpan frame)
+ {
+ Trace("Matching result...");
+
+ 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, frame, 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(frame, connectionType, _protocol);
+
+ [DoesNotReturn]
+ static void Throw(ReadOnlySpan frame, ConnectionType connection, RedisProtocol protocol)
+ {
+ var prefix = RespReaderExtensions.GetRespPrefix(frame);
+ throw new InvalidOperationException($"Received {connection}/{protocol} response with no message waiting: " + prefix.ToString());
+ }
+ }
+ }
+ _activeMessage = msg;
+
+ Trace("Response to: " + msg);
+ _readStatus = ReadStatus.ComputeResult;
+ var reader = new RespReader(frame);
+
+ OnDetailLog($"computing result for {msg.CommandAndKey} ({RespReaderExtensions.GetRespPrefix(frame)})");
+ if (msg.ComputeResult(this, ref reader))
+ {
+ OnDetailLog($"> complete: {msg.CommandAndKey}");
+ _readStatus = msg.ResultBoxIsAsync ? ReadStatus.CompletePendingMessageAsync : ReadStatus.CompletePendingMessageSync;
+ if (!msg.IsHighIntegrity)
+ {
+ // can't complete yet if needs checksum
+ msg.Complete();
+ }
+ }
+ else
+ {
+ OnDetailLog($"> incomplete: {msg.CommandAndKey}");
+ }
+ 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, ReadOnlySpan frame, PhysicalBridge? bridge)
+ {
+ bool isValid = false;
+ var reader = new RespReader(frame);
+ if ((reader.SafeTryMoveNext() & reader.IsScalar)
+ && reader.ScalarLength() is 4)
+ {
+ uint interpreted;
+ if (reader.TryGetSpan(out var span))
+ {
+ interpreted = BinaryPrimitives.ReadUInt32LittleEndian(span);
+ }
+ else
+ {
+ Span tmp = stackalloc byte[4];
+ reader.CopyTo(tmp);
+ interpreted = BinaryPrimitives.ReadUInt32LittleEndian(tmp);
+ }
+ 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;
+ }
+ }
+ }
+
+ private bool PeekChannelMessage(RedisCommand command, in RedisChannel channel)
+ {
+ Message? msg;
+ bool haveMsg;
+ lock (_writtenAwaitingResponse)
+ {
+ haveMsg = _writtenAwaitingResponse.TryPeek(out msg);
+ }
+
+ return haveMsg && msg is Message.CommandChannelBase typed
+ && typed.Command == command && typed.Channel == channel;
+ }
+
+ internal RedisChannel AsRedisChannel(in RespReader reader, RedisChannel.RedisChannelOptions options)
+ {
+ var channelPrefix = ChannelPrefix;
+ if (channelPrefix is null)
+ {
+ // no channel-prefix enabled, just use as-is
+ return new RedisChannel(reader.ReadByteArray(), options);
+ }
+
+ byte[] lease = [];
+ var span = reader.TryGetSpan(out var tmp) ? tmp : reader.Buffer(ref lease, stackalloc byte[256]);
+
+ if (span.StartsWith(channelPrefix))
+ {
+ // we have a channel-prefix, and it matches; strip it
+ span = span.Slice(channelPrefix.Length);
+ }
+ else if (span.StartsWith("__keyspace@"u8) || span.StartsWith("__keyevent@"u8))
+ {
+ // 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
+
+ // leave alone
+ }
+ else
+ {
+ // no idea what this is
+ span = default;
+ }
+
+ RedisChannel channel = span.IsEmpty ? default : new(span.ToArray(), options);
+ if (lease.Length != 0) ArrayPool.Shared.Return(lease);
+ return channel;
+ }
+
+ [AsciiHash("*2\r\n$4\r\npong\r\n$")]
+ private static partial class ArrayPong_LC_Bulk { }
+ [AsciiHash("*2\r\n$4\r\nPONG\r\n$")]
+ private static partial class ArrayPong_UC_Bulk { }
+ [AsciiHash("*2\r\n+pong\r\n$")]
+ private static partial class ArrayPong_LC_Simple { }
+ [AsciiHash("*2\r\n+PONG\r\n$")]
+ private static partial class ArrayPong_UC_Simple { }
+
+ // ReSharper disable InconsistentNaming
+ private static readonly uint
+ pong = RespConstants.UnsafeCpuUInt32("pong"u8),
+ PONG = RespConstants.UnsafeCpuUInt32("PONG"u8);
+
+ // ReSharper restore InconsistentNaming
+ [Conditional("DEBUG")]
+ private static void DebugValidateSingleFrame(ReadOnlySpan payload)
+ {
+ var reader = new RespReader(payload);
+ if (!reader.TryMoveNext(checkError: false))
+ {
+ throw new InvalidOperationException("No root RESP element");
+ }
+ reader.SkipChildren();
+
+#pragma warning disable CS0618 // we don't expect *any* additional data, even attributes
+ if (reader.TryReadNext())
+#pragma warning restore CS0618
+ {
+ throw new InvalidOperationException($"Unexpected trailing {reader.Prefix}");
+ }
+
+ if (reader.ProtocolBytesRemaining != 0)
+ {
+ var copy = reader; // leave reader alone for inspection
+ var prefix = copy.SafeTryMoveNext() ? copy.Prefix : RespPrefix.None;
+ throw new InvalidOperationException(
+ $"Unexpected additional {reader.ProtocolBytesRemaining} bytes remaining, {prefix}");
+ }
+ }
+}
diff --git a/src/StackExchange.Redis/PhysicalConnection.Write.cs b/src/StackExchange.Redis/PhysicalConnection.Write.cs
new file mode 100644
index 000000000..84d3430e1
--- /dev/null
+++ b/src/StackExchange.Redis/PhysicalConnection.Write.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Buffers;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace StackExchange.Redis;
+
+internal partial class PhysicalConnection
+{
+ private BufferedStreamWriter? _output;
+ private long TotalBytesSent => _output?.TotalBytesWritten ?? 0;
+ public IBufferWriter Output
+ {
+ get
+ {
+ return _output ?? Throw();
+ static IBufferWriter Throw() => throw new InvalidOperationException("Output pipe not initialized");
+ }
+ }
+
+ private void InitOutput(Stream? stream)
+ {
+ if (stream is null) return;
+ _ioStream = stream;
+ _output = BufferedStreamWriter.Create(WriteMode, connectionType, stream, OutputCancel);
+#if DEBUG
+ if (BridgeCouldBeNull?.Multiplexer.RawConfig.OutputLog is { } log)
+ {
+ _output.DebugSetLog(log);
+ }
+#endif
+ }
+
+ internal bool HasOutputPipe => _output is not null;
+
+ internal Task CompleteOutputAsync(Exception? exception = null)
+ {
+ if (_output is not { } output) return Task.CompletedTask;
+ output.Complete(exception);
+ return output.WriteComplete;
+ }
+}
diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs
index 6ba8b4cde..0ca75b4c1 100644
--- a/src/StackExchange.Redis/PhysicalConnection.cs
+++ b/src/StackExchange.Redis/PhysicalConnection.cs
@@ -1,11 +1,8 @@
using System;
-using System.Buffers;
-using System.Buffers.Binary;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
-using System.IO.Pipelines;
using System.Linq;
using System.Net;
using System.Net.Security;
@@ -19,13 +16,34 @@
using Microsoft.Extensions.Logging;
using Pipelines.Sockets.Unofficial;
using Pipelines.Sockets.Unofficial.Arenas;
-using RESPite;
+
using static StackExchange.Redis.Message;
namespace StackExchange.Redis
{
internal sealed partial class PhysicalConnection : IDisposable
{
+ // infrastructure to simulate connection death, debug only
+ private partial bool CanCancel();
+ [Conditional("DEBUG")]
+ partial void OnCancel(bool input, bool output);
+#if DEBUG
+ private readonly CancellationTokenSource _inputCancel = new(), _outputCancel = new();
+ internal CancellationToken InputCancel => _inputCancel.Token;
+ internal CancellationToken OutputCancel => _outputCancel.Token;
+
+ partial void OnCancel(bool input, bool output)
+ {
+ if (input) _inputCancel.Cancel();
+ if (output) _outputCancel.Cancel();
+ }
+ private partial bool CanCancel() => true;
+#else
+ private partial bool CanCancel() => false;
+ internal CancellationToken InputCancel => CancellationToken.None;
+ internal CancellationToken OutputCancel => CancellationToken.None;
+#endif
+
internal readonly byte[]? ChannelPrefix;
private const int DefaultRedisDatabaseCount = 16;
@@ -64,32 +82,44 @@ private static readonly Message
internal void GetBytes(out long sent, out long received)
{
- if (_ioPipe is IMeasuredDuplexPipe sc)
- {
- sent = sc.TotalBytesSent;
- received = sc.TotalBytesReceived;
- }
- else
- {
- sent = received = -1;
- }
+ sent = TotalBytesSent;
+ received = totalBytesReceived;
}
///
/// 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 Stream? _ioStream;
private Socket? _socket;
internal Socket? VolatileSocket => Volatile.Read(ref _socket);
- public PhysicalConnection(PhysicalBridge bridge)
+ // used for dummy test connections
+ public PhysicalConnection(
+ ConnectionType connectionType = ConnectionType.Interactive,
+ RedisProtocol protocol = RedisProtocol.Resp2,
+ Stream? ioStream = null,
+ BufferedStreamWriter.WriteMode writeMode = BufferedStreamWriter.WriteMode.Default,
+ [CallerMemberName] string name = "")
+ {
+ lastWriteTickCount = lastReadTickCount = Environment.TickCount;
+ lastBeatTickCount = 0;
+ this.connectionType = connectionType;
+ WriteMode = writeMode;
+ _protocol = protocol;
+ _bridge = new WeakReference(null);
+ _physicalName = name;
+ InitOutput(ioStream);
+ OnCreateEcho();
+ }
+
+ public PhysicalConnection(PhysicalBridge bridge, BufferedStreamWriter.WriteMode writeMode)
{
lastWriteTickCount = lastReadTickCount = Environment.TickCount;
lastBeatTickCount = 0;
connectionType = bridge.ConnectionType;
+ WriteMode = writeMode;
_bridge = new WeakReference(bridge);
ChannelPrefix = bridge.Multiplexer.ChannelPrefix;
if (ChannelPrefix?.Length == 0) ChannelPrefix = null; // null tests are easier than null+empty
@@ -121,7 +151,20 @@ internal async Task BeginConnectAsync(ILogger? log)
}
if (connectTo is not null)
{
- _socket = SocketManager.CreateSocket(connectTo);
+ _socket = CreateSocket(connectTo);
+
+ static Socket CreateSocket(EndPoint endpoint)
+ {
+ var addressFamily = endpoint.AddressFamily;
+ var protocolType = addressFamily == AddressFamily.Unix ? ProtocolType.Unspecified : ProtocolType.Tcp;
+
+ 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;
+ }
}
if (_socket is not null)
@@ -181,12 +224,12 @@ internal async Task BeginConnectAsync(ILogger? log)
{
ConnectionMultiplexer.TraceWithoutContext("Socket was already aborted");
}
- else if (await ConnectedAsync(x, log, bridge.Multiplexer.SocketManager!).ForAwait())
+ else if (await ConnectedAsync(x, log).ForAwait())
{
log?.LogInformationStartingRead(new(endpoint));
try
{
- StartReading();
+ StartReading(CancellationToken.None); // this already includes InputCancel
// Normal return
}
catch (Exception ex)
@@ -275,7 +318,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)
+ public void SetProtocol(RedisProtocol value)
{
_protocol = value;
BridgeCouldBeNull?.SetProtocol(value);
@@ -284,18 +327,14 @@ internal void SetProtocol(RedisProtocol value)
[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
+ var output = Interlocked.Exchange(ref _output, null); // compare to the critical read
var socket = Interlocked.Exchange(ref _socket, null);
- if (ioPipe != null)
+ if (output != null)
{
Trace("Disconnecting...");
try { BridgeCouldBeNull?.OnDisconnected(ConnectionFailureType.ConnectionDisposed, this, out _, out _); } catch { }
- try { ioPipe.Input?.CancelPendingRead(); } catch { }
- try { ioPipe.Input?.Complete(); } catch { }
- try { ioPipe.Output?.CancelPendingFlush(); } catch { }
- try { ioPipe.Output?.Complete(); } catch { }
- try { using (ioPipe as IDisposable) { } } catch { }
+ try { output.Complete(); } catch { }
}
if (socket != null)
@@ -316,66 +355,31 @@ public void Dispose()
RecordConnectionFailed(ConnectionFailureType.ConnectionDisposed);
}
OnCloseEcho();
- _arena.Dispose();
- _reusableFlushSyncTokenSource?.Dispose();
+ // ReSharper disable once GCSuppressFinalizeForTypeWithoutDestructor
GC.SuppressFinalize(this);
}
- private async Task AwaitedFlush(ValueTask flush)
- {
- await flush.ForAwait();
- _writeStatus = WriteStatus.Flushed;
- UpdateLastWriteTime();
- }
internal void UpdateLastWriteTime() => Interlocked.Exchange(ref lastWriteTickCount, Environment.TickCount);
- public Task FlushAsync()
- {
- var tmp = _ioPipe?.Output;
- if (tmp != null)
- {
- _writeStatus = WriteStatus.Flushing;
- var flush = tmp.FlushAsync();
- if (!flush.IsCompletedSuccessfully)
- {
- return AwaitedFlush(flush);
- }
- _writeStatus = WriteStatus.Flushed;
- UpdateLastWriteTime();
- }
- return Task.CompletedTask;
- }
+
+ internal bool CanSimulateConnectionFailure => false;
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)
+ bool killInput = false, killOutput = false;
+ switch (connectionType)
{
- 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;
- }
+ case ConnectionType.Interactive:
+ killInput = failureType.HasFlag(SimulatedFailureType.InteractiveInbound);
+ killOutput = failureType.HasFlag(SimulatedFailureType.InteractiveOutbound);
+ break;
+ case ConnectionType.Subscription:
+ killInput = failureType.HasFlag(SimulatedFailureType.SubscriptionInbound);
+ killOutput = failureType.HasFlag(SimulatedFailureType.SubscriptionOutbound);
+ break;
}
- if (raiseFailed)
+ if (killInput | killOutput)
{
+ OnCancel(killInput, killOutput);
RecordConnectionFailed(ConnectionFailureType.SocketFailure);
}
}
@@ -385,15 +389,14 @@ public void RecordConnectionFailed(
Exception? innerException = null,
[CallerMemberName] string? origin = null,
bool isInitialConnect = false,
- IDuplexPipe? connectingPipe = null)
+ Stream? connectingStream = null)
{
- bool weAskedForThis;
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 (_ioStream is not null || isInitialConnect) // if *we* didn't burn the pipe: flag it
{
if (failureType == ConnectionFailureType.InternalFailure && innerException is not null)
{
@@ -433,9 +436,10 @@ 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 = Volatile.Read(ref clientSentQuit) != 0;
+ var weAskedForThis = Volatile.Read(ref clientSentQuit) != 0;
- var pipe = connectingPipe ?? _ioPipe;
+ /*
+ var pipe = connectingStream ?? _ioStream;
if (pipe is SocketConnection sc)
{
exMessage.Append(" (").Append(sc.ShutdownKind);
@@ -454,9 +458,14 @@ public void RecordConnectionFailed(
if (sent == 0) { exMessage.Append(recd == 0 ? " (0-read, 0-sent)" : " (0-sent)"); }
else if (recd == 0) { exMessage.Append(" (0-read)"); }
}
+ */
+
+ long sent = TotalBytesSent, recd = totalBytesReceived;
+ if (sent == 0) { exMessage.Append(recd == 0 ? " (0-read, 0-sent)" : " (0-sent)"); }
+ else if (recd == 0) { exMessage.Append(" (0-read)"); }
var data = new List>();
- void AddData(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);
@@ -481,7 +490,6 @@ void AddData(string lk, string sk, string? v)
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());
@@ -600,10 +608,10 @@ internal static void IdentifyFailureType(Exception? exception, ref ConnectionFai
}
}
- internal void EnqueueInsideWriteLock(Message next)
+ internal void EnqueueInsideWriteLock(Message next, bool enforceMuxer = true)
{
var multiplexer = BridgeCouldBeNull?.Multiplexer;
- if (multiplexer is null)
+ if (multiplexer is null & enforceMuxer) // note: this should only be false for testing
{
// multiplexer already collected? then we're almost certainly doomed;
// we can still process it to avoid making things worse/more complex,
@@ -807,814 +815,193 @@ internal void SetUnknownDatabase()
currentDatabase = -1;
}
- internal void Write(in RedisKey key)
- {
- var val = key.KeyValue;
- if (val is string s)
- {
- WriteUnifiedPrefixedString(_ioPipe?.Output, key.KeyPrefix, s);
- }
- else
- {
- WriteUnifiedPrefixedBlob(_ioPipe?.Output, key.KeyPrefix, (byte[]?)val);
- }
- }
-
- internal void Write(in RedisChannel channel)
- => WriteUnifiedPrefixedBlob(_ioPipe?.Output, channel.IgnoreChannelPrefix ? null : ChannelPrefix, channel.Value);
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal void WriteBulkString(in RedisValue value)
- => WriteBulkString(value, _ioPipe?.Output);
- internal static void WriteBulkString(in RedisValue value, IBufferWriter? maybeNullWriter)
+ internal void RecordQuit()
{
- if (maybeNullWriter is not { } writer)
- {
- return; // Prevent null refs during disposal
- }
-
- switch (value.Type)
- {
- case RedisValue.StorageType.Null:
- WriteUnifiedBlob(writer, (byte[]?)null);
- break;
- case RedisValue.StorageType.Int64:
- WriteUnifiedInt64(writer, value.OverlappedValueInt64);
- break;
- case RedisValue.StorageType.UInt64:
- WriteUnifiedUInt64(writer, value.OverlappedValueUInt64);
- break;
- case RedisValue.StorageType.Double:
- WriteUnifiedDouble(writer, value.OverlappedValueDouble);
- break;
- case RedisValue.StorageType.String:
- WriteUnifiedPrefixedString(writer, null, (string?)value);
- break;
- case RedisValue.StorageType.Raw:
- WriteUnifiedSpan(writer, ((ReadOnlyMemory)value).Span);
- break;
- default:
- throw new InvalidOperationException($"Unexpected {value.Type} value: '{value}'");
- }
+ // don't blame redis if we fired the first shot
+ Volatile.Write(ref clientSentQuit, 1);
+ // (_ioPipe as SocketConnection)?.TrySetProtocolShutdown(PipeShutdownKind.ProtocolExitClient);
}
- internal void WriteBulkString(ReadOnlySpan value)
+ internal void Flush()
{
- if (_ioPipe?.Output is { } writer)
- {
- WriteUnifiedSpan(writer, value);
- }
+ var tmp = _output;
+ if (tmp is null) Throw();
+ _writeStatus = WriteStatus.Flushing;
+ tmp.Flush();
+ _writeStatus = WriteStatus.Flushed;
+ UpdateLastWriteTime();
+ [DoesNotReturn]
+ static void Throw() => throw new InvalidOperationException("Output pipe not initialized");
}
- 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)
+ internal readonly struct ConnectionStatus
{
- if (_ioPipe?.Output is not PipeWriter writer)
- {
- return; // Prevent null refs during disposal
- }
-
- var bridge = BridgeCouldBeNull ?? throw new ObjectDisposedException(ToString());
-
- if (command == RedisCommand.UNKNOWN)
- {
- // using >= here because we will be adding 1 for the command itself (which is an arg for the purposes of the multi-bulk protocol)
- if (arguments >= REDIS_MAX_ARGS) throw ExceptionFactory.TooManyArgs(commandBytes.ToString(), arguments);
- }
- else
- {
- // using >= here because we will be adding 1 for the command itself (which is an arg for the purposes of the multi-bulk protocol)
- if (arguments >= REDIS_MAX_ARGS) throw ExceptionFactory.TooManyArgs(command.ToString(), arguments);
-
- // for everything that isn't custom commands: ask the muxer for the actual bytes
- commandBytes = bridge.Multiplexer.CommandMap.GetBytes(command);
- }
+ ///
+ /// Number of messages sent outbound, but we don't yet have a response for.
+ ///
+ public int MessagesSentAwaitingResponse { get; init; }
- // in theory we should never see this; CheckMessage dealt with "regular" messages, and
- // ExecuteMessage should have dealt with everything else
- if (commandBytes.IsEmpty) throw ExceptionFactory.CommandDisabled(command);
+ ///
+ /// Bytes available on the socket, not yet read into the pipe.
+ ///
+ public long BytesAvailableOnSocket { get; init; }
- // *{argCount}\r\n = 3 + MaxInt32TextLen
- // ${cmd-len}\r\n = 3 + MaxInt32TextLen
- // {cmd}\r\n = 2 + commandBytes.Length
- var span = writer.GetSpan(commandBytes.Length + 8 + Format.MaxInt32TextLen + Format.MaxInt32TextLen);
- span[0] = (byte)'*';
+ ///
+ /// Bytes read from the socket, pending in the reader pipe.
+ ///
+ public long BytesInReadPipe { get; init; }
- int offset = WriteRaw(span, arguments + 1, offset: 1);
+ ///
+ /// Bytes in the writer pipe, waiting to be written to the socket.
+ ///
+ public long BytesInWritePipe { get; init; }
- offset = AppendToSpanCommand(span, commandBytes, offset: offset);
+ ///
+ /// Byte size of the last result we processed.
+ ///
+ public long BytesLastResult { get; init; }
- writer.Advance(offset);
- }
+ ///
+ /// Byte size on the buffer that isn't processed yet.
+ ///
+ public long BytesInBuffer { get; init; }
- internal void WriteRaw(ReadOnlySpan bytes) => _ioPipe?.Output?.Write(bytes);
+ ///
+ /// The inbound pipe reader status.
+ ///
+ public ReadStatus ReadStatus { get; init; }
- internal void RecordQuit()
- {
- // don't blame redis if we fired the first shot
- Volatile.Write(ref clientSentQuit, 1);
- (_ioPipe as SocketConnection)?.TrySetProtocolShutdown(PipeShutdownKind.ProtocolExitClient);
- }
+ ///
+ /// The outbound pipe writer status.
+ ///
+ public WriteStatus WriteStatus { get; init; }
- internal static void WriteMultiBulkHeader(IBufferWriter output, long count)
- {
- // *{count}\r\n = 3 + MaxInt32TextLen
- var span = output.GetSpan(3 + Format.MaxInt32TextLen);
- span[0] = (byte)'*';
- int offset = WriteRaw(span, count, offset: 1);
- output.Advance(offset);
- }
+ public override string ToString() =>
+ $"SentAwaitingResponse: {MessagesSentAwaitingResponse}, AvailableOnSocket: {BytesAvailableOnSocket} byte(s), InReadPipe: {BytesInReadPipe} byte(s), InWritePipe: {BytesInWritePipe} byte(s), ReadStatus: {ReadStatus}, WriteStatus: {WriteStatus}";
- internal static void WriteMultiBulkHeader(IBufferWriter output, long count, ResultType type)
- {
- // *{count}\r\n = 3 + MaxInt32TextLen
- var span = output.GetSpan(3 + Format.MaxInt32TextLen);
- span[0] = type switch
+ ///
+ /// The default connection stats, notable *not* the same as default since initializers don't run.
+ ///
+ public static ConnectionStatus Default { get; } = new()
{
- ResultType.Push => (byte)'>',
- ResultType.Attribute => (byte)'|',
- ResultType.Map => (byte)'%',
- ResultType.Set => (byte)'~',
- _ => (byte)'*',
+ BytesAvailableOnSocket = -1,
+ BytesInReadPipe = -1,
+ BytesInWritePipe = -1,
+ ReadStatus = ReadStatus.NA,
+ WriteStatus = WriteStatus.NA,
};
- 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)
- {
- span[offset++] = (byte)'\r';
- span[offset++] = (byte)'\n';
- return offset;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static void WriteCrlf(IBufferWriter writer)
- {
- var span = writer.GetSpan(2);
- span[0] = (byte)'\r';
- span[1] = (byte)'\n';
- writer.Advance(2);
+ ///
+ /// 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,
+ };
}
- internal static int WriteRaw(Span span, long value, bool withLengthPrefix = false, int offset = 0)
+ public ConnectionStatus GetStatus()
{
- if (value >= 0 && value <= 9)
- {
- if (withLengthPrefix)
- {
- span[offset++] = (byte)'1';
- offset = WriteCrlf(span, offset);
- }
- span[offset++] = (byte)((int)'0' + (int)value);
- }
- else if (value >= 10 && value < 100)
- {
- if (withLengthPrefix)
- {
- span[offset++] = (byte)'2';
- offset = WriteCrlf(span, offset);
- }
- span[offset++] = (byte)((int)'0' + ((int)value / 10));
- span[offset++] = (byte)((int)'0' + ((int)value % 10));
- }
- else if (value >= 100 && value < 1000)
- {
- int v = (int)value;
- int units = v % 10;
- v /= 10;
- int tens = v % 10, hundreds = v / 10;
- if (withLengthPrefix)
- {
- span[offset++] = (byte)'3';
- offset = WriteCrlf(span, offset);
- }
- span[offset++] = (byte)((int)'0' + hundreds);
- span[offset++] = (byte)((int)'0' + tens);
- span[offset++] = (byte)((int)'0' + units);
- }
- else if (value < 0 && value >= -9)
- {
- if (withLengthPrefix)
- {
- span[offset++] = (byte)'2';
- offset = WriteCrlf(span, offset);
- }
- span[offset++] = (byte)'-';
- span[offset++] = (byte)((int)'0' - (int)value);
- }
- else if (value <= -10 && value > -100)
+ // Fall back to bytes waiting on the socket if we can
+ int socketBytes;
+ try
{
- if (withLengthPrefix)
- {
- span[offset++] = (byte)'3';
- offset = WriteCrlf(span, offset);
- }
- value = -value;
- span[offset++] = (byte)'-';
- span[offset++] = (byte)((int)'0' + ((int)value / 10));
- span[offset++] = (byte)((int)'0' + ((int)value % 10));
+ socketBytes = VolatileSocket?.Available ?? -1;
}
- else
+ catch
{
- // we're going to write it, but *to the wrong place*
- var availableChunk = span.Slice(offset);
- var formattedLength = Format.FormatInt64(value, availableChunk);
- if (withLengthPrefix)
- {
- // now we know how large the prefix is: write the prefix, then write the value
- var prefixLength = Format.FormatInt32(formattedLength, availableChunk);
- offset += prefixLength;
- offset = WriteCrlf(span, offset);
-
- availableChunk = span.Slice(offset);
- var finalLength = Format.FormatInt64(value, availableChunk);
- offset += finalLength;
- Debug.Assert(finalLength == formattedLength);
- }
- else
- {
- offset += formattedLength;
- }
+ // If this fails, we're likely in a race disposal situation and do not want to blow sky high here.
+ socketBytes = -1;
}
- return WriteCrlf(span, offset);
+ return new ConnectionStatus()
+ {
+ BytesAvailableOnSocket = socketBytes,
+ BytesInReadPipe = GetReadCommittedLength(),
+ BytesInWritePipe = -1,
+ ReadStatus = _readStatus,
+ WriteStatus = _writeStatus,
+ BytesLastResult = bytesLastResult,
+ BytesInBuffer = bytesInBuffer,
+ };
}
- [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)
+ internal static RemoteCertificateValidationCallback? GetAmbientIssuerCertificateCallback()
{
try
{
- await flush.ForAwait();
- connection._writeStatus = WriteStatus.Flushed;
- connection.UpdateLastWriteTime();
- return WriteResult.Success;
+ var issuerPath = Environment.GetEnvironmentVariable("SERedis_IssuerCertPath");
+ if (!string.IsNullOrEmpty(issuerPath)) return ConfigurationOptions.TrustIssuerCallback(issuerPath);
}
- catch (ConnectionResetException ex) when (!throwOnFailure)
+ catch (Exception ex)
{
- connection.RecordConnectionFailed(ConnectionFailureType.SocketClosed, ex);
- return WriteResult.WriteFailure;
+ Debug.WriteLine(ex.Message);
}
+ return null;
}
-
- 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)
+ internal static LocalCertificateSelectionCallback? GetAmbientClientCertificateCallback()
{
- var cts = _reusableFlushSyncTokenSource ??= new CancellationTokenSource();
- var flush = FlushAsync(throwOnFailure, cts.Token);
- if (!flush.IsCompletedSuccessfully)
+ try
{
- // only schedule cancellation if it doesn't complete synchronously; at this point, it is doomed
- _reusableFlushSyncTokenSource = null;
- cts.CancelAfter(TimeSpan.FromMilliseconds(millisecondsTimeout));
- try
- {
- // here lies the evil
- flush.AsTask().Wait();
- }
- catch (AggregateException ex) when (ex.InnerExceptions.Any(e => e is TaskCanceledException))
+ var certificatePath = Environment.GetEnvironmentVariable("SERedis_ClientCertPfxPath");
+ if (!string.IsNullOrEmpty(certificatePath) && File.Exists(certificatePath))
{
- ThrowTimeout();
+ 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);
}
- finally
+
+#if NET
+ certificatePath = Environment.GetEnvironmentVariable("SERedis_ClientCertPemPath");
+ if (!string.IsNullOrEmpty(certificatePath) && File.Exists(certificatePath))
{
- cts.Dispose();
+ var passwordPath = Environment.GetEnvironmentVariable("SERedis_ClientCertPasswordPath");
+ return ConfigurationOptions.CreatePemUserCertificateCallback(certificatePath, passwordPath);
}
+#endif
}
- return flush.Result;
-
- void ThrowTimeout()
- {
- throw new TimeoutException("timeout while synchronously flushing");
- }
- }
- internal ValueTask FlushAsync(bool throwOnFailure, CancellationToken cancellationToken = default)
- {
- var tmp = _ioPipe?.Output;
- if (tmp == null) return new ValueTask(WriteResult.NoConnectionAvailable);
- try
- {
- _writeStatus = WriteStatus.Flushing;
- var flush = tmp.FlushAsync(cancellationToken);
- if (!flush.IsCompletedSuccessfully) return FlushAsync_Awaited(this, flush, throwOnFailure);
- _writeStatus = WriteStatus.Flushed;
- UpdateLastWriteTime();
- return new ValueTask(WriteResult.Success);
- }
- catch (ConnectionResetException ex) when (!throwOnFailure)
+ catch (Exception ex)
{
- RecordConnectionFailed(ConnectionFailureType.SocketClosed, ex);
- return new ValueTask(WriteResult.WriteFailure);
+ Debug.WriteLine(ex.Message);
}
+ return null;
}
- private static readonly ReadOnlyMemory NullBulkString = Encoding.ASCII.GetBytes("$-1\r\n"), EmptyBulkString = Encoding.ASCII.GetBytes("$0\r\n\r\n");
-
- private static void WriteUnifiedBlob(IBufferWriter writer, byte[]? value)
+ internal async ValueTask ConnectedAsync(Socket? socket, ILogger? log)
{
- if (value == null)
- {
- // special case:
- writer.Write(NullBulkString.Span);
- }
- else
- {
- WriteUnifiedSpan(writer, new ReadOnlySpan(value));
- }
- }
+ var bridge = BridgeCouldBeNull;
+ if (bridge == null) return false;
- private static void WriteUnifiedSpan(IBufferWriter writer, ReadOnlySpan value)
- {
- // ${len}\r\n = 3 + MaxInt32TextLen
- // {value}\r\n = 2 + value.Length
- const int MaxQuickSpanSize = 512;
- if (value.Length == 0)
- {
- // special case:
- writer.Write(EmptyBulkString.Span);
- }
- else if (value.Length <= MaxQuickSpanSize)
- {
- var span = writer.GetSpan(5 + Format.MaxInt32TextLen + value.Length);
- span[0] = (byte)'$';
- int bytes = AppendToSpan(span, value, 1);
- writer.Advance(bytes);
- }
- else
+ Stream? stream = null;
+ try
{
- // too big to guarantee can do in a single span
- var span = writer.GetSpan(3 + Format.MaxInt32TextLen);
- span[0] = (byte)'$';
- int bytes = WriteRaw(span, value.Length, offset: 1);
- writer.Advance(bytes);
+ // disallow connection in some cases
+ OnDebugAbort();
- writer.Write(value);
-
- WriteCrlf(writer);
- }
- }
-
- private static int AppendToSpanCommand(Span span, in CommandBytes value, int offset = 0)
- {
- span[offset++] = (byte)'$';
- int len = value.Length;
- offset = WriteRaw(span, len, offset: offset);
- value.CopyTo(span.Slice(offset, len));
- offset += value.Length;
- return WriteCrlf(span, offset);
- }
-
- private static int AppendToSpan(Span span, ReadOnlySpan value, int offset = 0)
- {
- offset = WriteRaw(span, value.Length, offset: offset);
- value.CopyTo(span.Slice(offset, value.Length));
- offset += value.Length;
- return WriteCrlf(span, offset);
- }
-
- internal void WriteSha1AsHex(byte[] value)
- {
- if (_ioPipe?.Output is not PipeWriter writer)
- {
- return; // Prevent null refs during disposal
- }
-
- if (value == null)
- {
- writer.Write(NullBulkString.Span);
- }
- else if (value.Length == ResultProcessor.ScriptLoadProcessor.Sha1HashLength)
- {
- // $40\r\n = 5
- // {40 bytes}\r\n = 42
- var span = writer.GetSpan(47);
- span[0] = (byte)'$';
- span[1] = (byte)'4';
- span[2] = (byte)'0';
- span[3] = (byte)'\r';
- span[4] = (byte)'\n';
-
- int offset = 5;
- for (int i = 0; i < value.Length; i++)
- {
- var b = value[i];
- span[offset++] = ToHexNibble(b >> 4);
- span[offset++] = ToHexNibble(b & 15);
- }
- span[offset++] = (byte)'\r';
- span[offset++] = (byte)'\n';
-
- writer.Advance(offset);
- }
- else
- {
- throw new InvalidOperationException("Invalid SHA1 length: " + value.Length);
- }
- }
-
- internal static byte ToHexNibble(int value)
- {
- return value < 10 ? (byte)('0' + value) : (byte)('a' - 10 + value);
- }
-
- internal static void WriteUnifiedPrefixedString(IBufferWriter? maybeNullWriter, byte[]? prefix, string? value)
- {
- if (maybeNullWriter is not { } writer)
- {
- return; // Prevent null refs during disposal
- }
-
- if (value == null)
- {
- // special case
- writer.Write(NullBulkString.Span);
- }
- else
- {
- // ${total-len}\r\n 3 + MaxInt32TextLen
- // {prefix}{value}\r\n
- int encodedLength = Encoding.UTF8.GetByteCount(value),
- prefixLength = prefix?.Length ?? 0,
- totalLength = prefixLength + encodedLength;
-
- if (totalLength == 0)
- {
- // special-case
- writer.Write(EmptyBulkString.Span);
- }
- else
- {
- var span = writer.GetSpan(3 + Format.MaxInt32TextLen);
- span[0] = (byte)'$';
- int bytes = WriteRaw(span, totalLength, offset: 1);
- writer.Advance(bytes);
-
- if (prefixLength != 0) writer.Write(prefix);
- if (encodedLength != 0) WriteRaw(writer, value, encodedLength);
- WriteCrlf(writer);
- }
- }
- }
-
- [ThreadStatic]
- private static Encoder? s_PerThreadEncoder;
- internal static Encoder GetPerThreadEncoder()
- {
- var encoder = s_PerThreadEncoder;
- if (encoder == null)
- {
- s_PerThreadEncoder = encoder = Encoding.UTF8.GetEncoder();
- }
- else
- {
- encoder.Reset();
- }
- return encoder;
- }
-
- internal static unsafe void WriteRaw(IBufferWriter writer, string value, int expectedLength)
- {
- const int MaxQuickEncodeSize = 512;
-
- fixed (char* cPtr = value)
- {
- int totalBytes;
- if (expectedLength <= MaxQuickEncodeSize)
- {
- // encode directly in one hit
- var span = writer.GetSpan(expectedLength);
- fixed (byte* bPtr = span)
- {
- totalBytes = Encoding.UTF8.GetBytes(cPtr, value.Length, bPtr, expectedLength);
- }
- writer.Advance(expectedLength);
- }
- else
- {
- // use an encoder in a loop
- var encoder = GetPerThreadEncoder();
- int charsRemaining = value.Length, charOffset = 0;
- totalBytes = 0;
-
- bool final = false;
- while (true)
- {
- var span = writer.GetSpan(5); // get *some* memory - at least enough for 1 character (but hopefully lots more)
-
- int charsUsed, bytesUsed;
- bool completed;
- fixed (byte* bPtr = span)
- {
- encoder.Convert(cPtr + charOffset, charsRemaining, bPtr, span.Length, final, out charsUsed, out bytesUsed, out completed);
- }
- writer.Advance(bytesUsed);
- totalBytes += bytesUsed;
- charOffset += charsUsed;
- charsRemaining -= charsUsed;
-
- if (charsRemaining <= 0)
- {
- if (charsRemaining < 0) throw new InvalidOperationException("String encode went negative");
- if (completed) break; // fine
- if (final) throw new InvalidOperationException("String encode failed to complete");
- final = true; // flush the encoder to one more span, then exit
- }
- }
- }
- if (totalBytes != expectedLength) throw new InvalidOperationException("String encode length check failure");
- }
- }
-
- 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)
- {
- // 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);
- }
- else
- {
- 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);
-
- writer.Write(prefix);
- writer.Write(value);
-
- span = writer.GetSpan(2);
- WriteCrlf(span, 0);
- writer.Advance(2);
- }
- }
-
- 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"
-
- // ${asc-len}\r\n = 4/5 (asc-len at most 2 digits)
- // {asc}\r\n = MaxInt64TextLen + 2
- var span = writer.GetSpan(7 + Format.MaxInt64TextLen);
-
- span[0] = (byte)'$';
- var bytes = WriteRaw(span, value, withLengthPrefix: true, offset: 1);
- writer.Advance(bytes);
- }
-
- 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"
- 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(IBufferWriter 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(IBufferWriter writer, long value)
- {
- // note: client should never write integer; only server does this
- // :{asc}\r\n = MaxInt64TextLen + 3
- var span = writer.GetSpan(3 + Format.MaxInt64TextLen);
-
- span[0] = (byte)':';
- var bytes = WriteRaw(span, value, withLengthPrefix: false, offset: 1);
- writer.Advance(bytes);
- }
-
- 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; }
-
- ///
- /// 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.
- ///
- public ReadStatus ReadStatus { get; init; }
-
- ///
- /// The outbound pipe writer status.
- ///
- 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.
- ///
- 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();
- return new ConnectionStatus()
- {
- MessagesSentAwaitingResponse = GetSentAwaitingResponseCount(),
- BytesAvailableOnSocket = counters.BytesAvailableOnSocket,
- BytesInReadPipe = counters.BytesWaitingToBeRead,
- BytesInWritePipe = counters.BytesWaitingToBeSent,
- ReadStatus = _readStatus,
- WriteStatus = _writeStatus,
- BytesLastResult = bytesLastResult,
- BytesInBuffer = bytesInBuffer,
- };
- }
-
- // Fall back to bytes waiting on the socket if we can
- int fallbackBytesAvailable;
- try
- {
- 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.
- fallbackBytesAvailable = -1;
- }
-
- return new ConnectionStatus()
- {
- BytesAvailableOnSocket = fallbackBytesAvailable,
- BytesInReadPipe = -1,
- BytesInWritePipe = -1,
- ReadStatus = _readStatus,
- WriteStatus = _writeStatus,
- BytesLastResult = bytesLastResult,
- BytesInBuffer = bytesInBuffer,
- };
- }
-
- internal static RemoteCertificateValidationCallback? GetAmbientIssuerCertificateCallback()
- {
- try
- {
- var issuerPath = Environment.GetEnvironmentVariable("SERedis_IssuerCertPath");
- if (!string.IsNullOrEmpty(issuerPath)) return ConfigurationOptions.TrustIssuerCallback(issuerPath);
- }
- catch (Exception ex)
- {
- Debug.WriteLine(ex.Message);
- }
- return null;
- }
- internal static LocalCertificateSelectionCallback? GetAmbientClientCertificateCallback()
- {
- try
- {
- var certificatePath = Environment.GetEnvironmentVariable("SERedis_ClientCertPfxPath");
- if (!string.IsNullOrEmpty(certificatePath) && File.Exists(certificatePath))
- {
- 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 NET
- certificatePath = Environment.GetEnvironmentVariable("SERedis_ClientCertPemPath");
- if (!string.IsNullOrEmpty(certificatePath) && File.Exists(certificatePath))
- {
- var passwordPath = Environment.GetEnvironmentVariable("SERedis_ClientCertPasswordPath");
- return ConfigurationOptions.CreatePemUserCertificateCallback(certificatePath, passwordPath);
- }
-#endif
- }
- catch (Exception ex)
- {
- Debug.WriteLine(ex.Message);
- }
- return null;
- }
-
- internal async ValueTask ConnectedAsync(Socket? socket, ILogger? log, SocketManager manager)
- {
- var bridge = BridgeCouldBeNull;
- if (bridge == null) return false;
-
- IDuplexPipe? pipe = null;
- try
- {
- // disallow connection in some cases
- OnDebugAbort();
-
- // the order is important here:
- // non-TLS: [Socket]<==[SocketConnection:IDuplexPipe]
- // TLS: [Socket]<==[NetworkStream]<==[SslStream]<==[StreamConnection:IDuplexPipe]
- var config = bridge.Multiplexer.RawConfig;
+ // 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;
- Stream? stream = null;
if (tunnel is not null)
{
stream = await tunnel.BeforeAuthenticateAsync(bridge.ServerEndPoint.EndPoint, bridge.ConnectionType, socket, CancellationToken.None).ForAwait();
}
+ static Stream DemandSocketStream(Socket? socket)
+ => new NetworkStream(socket ?? throw new InvalidOperationException("No socket or stream available - possibly a tunnel error"));
+
if (config.Ssl)
{
log?.LogInformationConfiguringTLS();
@@ -1624,7 +1011,7 @@ internal async ValueTask ConnectedAsync(Socket? socket, ILogger? log, Sock
host = Format.ToStringHostOnly(bridge.ServerEndPoint.EndPoint);
}
- stream ??= new NetworkStream(socket ?? throw new InvalidOperationException("No socket or stream available - possibly a tunnel error"));
+ stream ??= DemandSocketStream(socket);
var ssl = new SslStream(
innerStream: stream,
leaveInnerStreamOpen: false,
@@ -1667,17 +1054,10 @@ internal async ValueTask ConnectedAsync(Socket? socket, ILogger? log, Sock
stream = ssl;
}
- if (stream is not null)
- {
- pipe = StreamConnection.GetDuplex(stream, manager.SendPipeOptions, manager.ReceivePipeOptions, name: bridge.Name);
- }
- else
- {
- pipe = SocketConnection.Create(socket, manager.SendPipeOptions, manager.ReceivePipeOptions, name: bridge.Name);
- }
- OnWrapForLogging(ref pipe, _physicalName, manager);
+ stream ??= DemandSocketStream(socket);
+ OnWrapForLogging(ref stream, _physicalName);
- _ioPipe = pipe;
+ InitOutput(stream);
log?.LogInformationConnected(bridge.Name);
@@ -1686,318 +1066,12 @@ internal async ValueTask ConnectedAsync(Socket? socket, ILogger? log, Sock
}
catch (Exception ex)
{
- RecordConnectionFailed(ConnectionFailureType.InternalFailure, ex, isInitialConnect: true, connectingPipe: pipe); // includes a bridge.OnDisconnected
+ RecordConnectionFailed(ConnectionFailureType.InternalFailure, ex, isInitialConnect: true, connectingStream: stream); // includes a bridge.OnDisconnected
bridge.Multiplexer.Trace("Could not connect: " + ex.Message, ToString());
return false;
}
}
- 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;
- if (len < 2)
- {
- // for supported cases, we demand at least the kind and the subscription channel
- channel = default;
- return PushKind.None;
- }
-
- if (result[0].TryParse(PushKindMetadata.TryParse, out PushKind kind) && kind is not PushKind.None)
- {
- RedisChannel.RedisChannelOptions channelOptions = RedisChannel.RedisChannelOptions.None;
- switch (kind)
- {
- case PushKind.Message when len >= 3:
- break;
- case PushKind.PMessage when len >= 4:
- channelOptions = RedisChannel.RedisChannelOptions.Pattern;
- break;
- case PushKind.SMessage when len >= 3:
- channelOptions = RedisChannel.RedisChannelOptions.Sharded;
- break;
- case PushKind.Subscribe:
- break;
- case PushKind.PSubscribe:
- channelOptions = RedisChannel.RedisChannelOptions.Pattern;
- break;
- case PushKind.SSubscribe:
- channelOptions = RedisChannel.RedisChannelOptions.Sharded;
- break;
- case PushKind.Unsubscribe:
- break;
- case PushKind.PUnsubscribe:
- channelOptions = RedisChannel.RedisChannelOptions.Pattern;
- break;
- case PushKind.SUnsubscribe:
- channelOptions = RedisChannel.RedisChannelOptions.Sharded;
- 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;
- }
-
- private void MatchResult(in RawResult result)
- {
- // check to see if it could be an out-of-band pubsub message
- if ((connectionType == ConnectionType.Subscription && result.Resp2TypeArray == ResultType.Array) || result.Resp3Type == ResultType.Push)
- {
- var muxer = BridgeCouldBeNull?.Multiplexer;
- if (muxer == null) return;
-
- // out of band message does not match to a queued message
- var items = result.GetItems();
- var kind = GetPushKind(items, out var subscriptionChannel);
- switch (kind)
- {
- 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
- {
- 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 */
- }
-
- Trace("Configuration changed: " + Format.ToString(blame));
- _readStatus = ReadStatus.Reconfigure;
- muxer.ReconfigureIfNeeded(blame, true, "broadcast");
- }
-
- // invoke the handlers
- if (!subscriptionChannel.IsNull)
- {
- 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
- case PushKind.PMessage:
- _readStatus = ReadStatus.PubSubPMessage;
-
- var messageChannel = items[2].AsRedisChannel(ChannelPrefix, RedisChannel.RedisChannelOptions.None);
- if (!messageChannel.IsNull)
- {
- 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);
- }
- }
- 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))
- {
- // 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: "sunsubscribe");
- }
- return; // and STOP PROCESSING; unsolicited
- }
- }
- Trace("Matching result...");
-
- 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());
- }
- }
- _activeMessage = msg;
-
- Trace("Response to: " + msg);
- _readStatus = ReadStatus.ComputeResult;
- if (msg.ComputeResult(this, result))
- {
- _readStatus = msg.ResultBoxIsAsync ? ReadStatus.CompletePendingMessageAsync : ReadStatus.CompletePendingMessageSync;
- 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)
- {
- parsed = RedisValue.Null;
- return true;
- }
- switch (value.Resp2TypeBulkString)
- {
- case ResultType.Integer:
- case ResultType.SimpleString:
- case ResultType.BulkString:
- parsed = value.AsRedisValue();
- return true;
- case ResultType.Array when allowArraySingleton && value.ItemsCount == 1:
- return TryGetPubSubPayload(in value[0], out parsed, allowArraySingleton: false);
- }
- parsed = default;
- return false;
- }
-
- static bool TryGetMultiPubSubPayload(in RawResult value, out Sequence parsed)
- {
- if (value.Resp2TypeArray == ResultType.Array && value.ItemsCount != 0)
- {
- parsed = value.GetItems();
- return true;
- }
- parsed = default;
- return false;
- }
- }
-
- private bool PeekChannelMessage(RedisCommand command, in 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)
@@ -2036,234 +1110,9 @@ private void OnDebugAbort()
}
}
- partial void OnWrapForLogging(ref IDuplexPipe pipe, string name, SocketManager mgr);
+ partial void OnWrapForLogging(ref Stream stream, string name);
internal void UpdateLastReadTime() => Interlocked.Exchange(ref lastReadTickCount, Environment.TickCount);
- private async Task ReadFromPipe()
- {
- bool allowSyncRead = true, isReading = false;
- try
- {
- _readStatus = ReadStatus.Init;
- while (true)
- {
- var input = _ioPipe?.Input;
- if (input == null) break;
-
- // note: TryRead will give us back the same buffer in a tight loop
- // - so: only use that if we're making progress
- isReading = true;
- _readStatus = ReadStatus.ReadSync;
- if (!(allowSyncRead && input.TryRead(out var readResult)))
- {
- _readStatus = ReadStatus.ReadAsync;
- readResult = await input.ReadAsync().ForAwait();
- }
- isReading = false;
- _readStatus = ReadStatus.UpdateWriteTime;
- UpdateLastReadTime();
-
- _readStatus = ReadStatus.ProcessBuffer;
- var buffer = readResult.Buffer;
- int handled = 0;
- if (!buffer.IsEmpty)
- {
- handled = ProcessBuffer(ref buffer); // updates buffer.Start
- }
-
- allowSyncRead = handled != 0;
-
- _readStatus = ReadStatus.MarkProcessed;
- Trace($"Processed {handled} messages");
- input.AdvanceTo(buffer.Start, buffer.End);
-
- if ((handled == 0 && readResult.IsCompleted) || BridgeCouldBeNull?.NeedsReconnect == true)
- {
- break; // no more data, trailing incomplete messages, or reconnection required
- }
- }
- Trace("EOF");
- RecordConnectionFailed(ConnectionFailureType.SocketClosed);
- _readStatus = ReadStatus.RanToCompletion;
- }
- catch (Exception ex)
- {
- _readStatus = ReadStatus.Faulted;
- // this CEX is just a hardcore "seriously, read the actual value" - there's no
- // convenient "Thread.VolatileRead(ref T field) where T : class", and I don't
- // want to make the field volatile just for this one place that needs it
- if (isReading)
- {
- var pipe = Volatile.Read(ref _ioPipe);
- if (pipe == null)
- {
- return;
- // yeah, that's fine... don't worry about it; we nuked it
- }
-
- // check for confusing read errors - no need to present "Reading is not allowed after reader was completed."
- if (pipe is SocketConnection sc && sc.ShutdownKind == PipeShutdownKind.ReadEndOfStream)
- {
- RecordConnectionFailed(ConnectionFailureType.SocketClosed, new EndOfStreamException());
- return;
- }
- }
- Trace("Faulted");
- RecordConnectionFailed(ConnectionFailureType.InternalFailure, ex);
- }
- }
-
- 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;
- bytesInBuffer = buffer.Length;
-
- while (!buffer.IsEmpty)
- {
- _readStatus = ReadStatus.TryParseResult;
- var reader = new BufferReader(buffer);
- var result = TryParseResult(_protocol >= RedisProtocol.Resp3, _arena, in buffer, ref reader, IncludeDetailInExceptions, this);
- try
- {
- if (result.HasValue)
- {
- buffer = reader.SliceFromCurrent();
-
- messageCount++;
- 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
- {
- break; // remaining buffer isn't enough; give up
- }
- }
- finally
- {
- _readStatus = ReadStatus.ResetArena;
- _arena.Reset();
- }
- }
- _readStatus = ReadStatus.ProcessBufferComplete;
- return messageCount;
- }
-
- 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, 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);
- }
-
- int itemCountActual = checked((int)i64);
-
- if (itemCountActual < 0)
- {
- // 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
- 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(resultType, oversized, flags);
-
- if (oversized.IsSingleSegment)
- {
- var span = oversized.FirstSpan;
- for (int i = 0; i < span.Length; i++)
- {
- if (!(span[i] = TryParseResult(flags, arena, in buffer, ref reader, includeDetailInExceptions, server)).HasValue)
- {
- return RawResult.Nil;
- }
- }
- }
- else
- {
- foreach (var span in oversized.Spans)
- {
- for (int i = 0; i < span.Length; i++)
- {
- if (!(span[i] = TryParseResult(flags, arena, in buffer, ref reader, includeDetailInExceptions, server)).HasValue)
- {
- return RawResult.Nil;
- }
- }
- }
- }
- return result;
- }
- return RawResult.Nil;
- }
-
- private static RawResult ReadBulkString(ResultType type, RawResult.ResultFlags flags, ref BufferReader reader, bool includeDetailInExceptions, ServerEndPoint? server)
- {
- var prefix = ReadLineTerminatedString(ResultType.Integer, flags, ref reader);
- if (prefix.HasValue)
- {
- if (!prefix.TryGetInt64(out long i64))
- {
- 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)
- {
- return new RawResult(type, ReadOnlySequence.Empty, AsNull(flags));
- }
-
- if (reader.TryConsumeAsBuffer(bodySize, out var payload))
- {
- switch (reader.TryConsumeCRLF())
- {
- case ConsumeResult.NeedMoreData:
- break; // see NilResult below
- case ConsumeResult.Success:
- return new RawResult(type, payload, flags);
- default:
- throw ExceptionFactory.ConnectionFailure(includeDetailInExceptions, ConnectionFailureType.ProtocolFailure, "Invalid bulk string terminator", server);
- }
- }
- }
- return RawResult.Nil;
- }
-
- private static RawResult ReadLineTerminatedString(ResultType type, RawResult.ResultFlags flags, ref BufferReader reader)
- {
- int crlfOffsetFromCurrent = BufferReader.FindNextCrLf(reader);
- if (crlfOffsetFromCurrent < 0) return RawResult.Nil;
-
- var payload = reader.ConsumeAsBuffer(crlfOffsetFromCurrent);
- reader.Consume(2);
-
- return new RawResult(type, payload, flags);
- }
internal enum ReadStatus
{
@@ -2294,122 +1143,6 @@ internal enum ReadStatus
PubSubUnsubscribe,
NA = -1,
}
- private volatile ReadStatus _readStatus;
- internal ReadStatus GetReadStatus() => _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)
- {
- 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(RawResult.ResultFlags flags, Arena arena, in RawResult line)
- {
- if (!line.HasValue) return RawResult.Nil; // incomplete line
-
- int count = 0;
- foreach (var _ in line.GetInlineTokenizer()) count++;
- var block = arena.Allocate(count);
-
- 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.Resp3Type, token, flags); // spoof RESP2 from RESP1
- }
- return new RawResult(ResultType.Array, block, flags); // spoof RESP2 from RESP1
- }
internal bool HasPendingCallerFacingItems()
{
diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt
index ab058de62..6c180196d 100644
--- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt
+++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt
@@ -1 +1,19 @@
#nullable enable
+[SER005]StackExchange.Redis.TestHarness
+[SER005]StackExchange.Redis.TestHarness.BufferValidator
+[SER005]StackExchange.Redis.TestHarness.ChannelPrefix.get -> StackExchange.Redis.RedisChannel
+[SER005]StackExchange.Redis.TestHarness.CommandMap.get -> StackExchange.Redis.CommandMap!
+[SER005]StackExchange.Redis.TestHarness.KeyPrefix.get -> StackExchange.Redis.RedisKey
+[SER005]StackExchange.Redis.TestHarness.Read(System.ReadOnlySpan value) -> StackExchange.Redis.RedisResult!
+[SER005]StackExchange.Redis.TestHarness.TestHarness(StackExchange.Redis.CommandMap? commandMap = null, StackExchange.Redis.RedisChannel channelPrefix = default(StackExchange.Redis.RedisChannel), StackExchange.Redis.RedisKey keyPrefix = default(StackExchange.Redis.RedisKey)) -> void
+[SER005]StackExchange.Redis.TestHarness.ValidateResp(string! expected, string! command, params System.Collections.Generic.ICollection