Skip to content

Commit 35beeeb

Browse files
authored
Timeouts: add "last-in" for the last payload's length to exception messages (#2276)
In a multiplexed setup we often see a timeout behind a large payload in the process of parsing (or not). This is meant to help better diagnose that case, by recording what the last payload size was that came through this connection. Needs some @mgravell eyes on the record point here and if we want to adjust (or if it's wrong due to buffers in a way I'm not understanding). I realize this won't work on a payload so large enough it blows all buffers completely, but I don't see how to solve that either the way we properly stream parse. I think it'll still improve most cases we see this in.
1 parent d90b21c commit 35beeeb

File tree

5 files changed

+37
-9
lines changed

5 files changed

+37
-9
lines changed

docs/ReleaseNotes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Current package versions:
88

99
## Unreleased
1010

11-
No pending changes for the next release yet.
11+
- Adds: `last-in` and `cur-in` (bytes) to timeout exceptions to help identify timeouts that were just-behind another large payload off the wire ([#2276 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2276))
1212

1313
## 2.6.70
1414

src/StackExchange.Redis/ExceptionFactory.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,8 @@ private static void AddCommonDetail(
320320
if (bs.Connection.BytesAvailableOnSocket >= 0) Add(data, sb, "Inbound-Bytes", "in", bs.Connection.BytesAvailableOnSocket.ToString());
321321
if (bs.Connection.BytesInReadPipe >= 0) Add(data, sb, "Inbound-Pipe-Bytes", "in-pipe", bs.Connection.BytesInReadPipe.ToString());
322322
if (bs.Connection.BytesInWritePipe >= 0) Add(data, sb, "Outbound-Pipe-Bytes", "out-pipe", bs.Connection.BytesInWritePipe.ToString());
323+
Add(data, sb, "Last-Result-Bytes", "last-in", bs.Connection.BytesLastResult.ToString());
324+
Add(data, sb, "Inbound-Buffer-Bytes", "cur-in", bs.Connection.BytesInBuffer.ToString());
323325

324326
if (multiplexer.StormLogThreshold >= 0 && bs.Connection.MessagesSentAwaitingResponse >= multiplexer.StormLogThreshold && Interlocked.CompareExchange(ref multiplexer.haveStormLog, 1, 0) == 0)
325327
{

src/StackExchange.Redis/PhysicalConnection.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ private static readonly Message
5252

5353
private int lastWriteTickCount, lastReadTickCount, lastBeatTickCount;
5454

55+
private long bytesLastResult;
56+
private long bytesInBuffer;
57+
5558
internal void GetBytes(out long sent, out long received)
5659
{
5760
if (_ioPipe is IMeasuredDuplexPipe sc)
@@ -1283,6 +1286,14 @@ internal readonly struct ConnectionStatus
12831286
/// Bytes in the writer pipe, waiting to be written to the socket.
12841287
/// </summary>
12851288
public long BytesInWritePipe { get; init; }
1289+
/// <summary>
1290+
/// Byte size of the last result we processed.
1291+
/// </summary>
1292+
public long BytesLastResult { get; init; }
1293+
/// <summary>
1294+
/// Byte size on the buffer that isn't processed yet.
1295+
/// </summary>
1296+
public long BytesInBuffer { get; init; }
12861297

12871298
/// <summary>
12881299
/// The inbound pipe reader status.
@@ -1334,6 +1345,8 @@ public ConnectionStatus GetStatus()
13341345
BytesInWritePipe = counters.BytesWaitingToBeSent,
13351346
ReadStatus = _readStatus,
13361347
WriteStatus = _writeStatus,
1348+
BytesLastResult = bytesLastResult,
1349+
BytesInBuffer = bytesInBuffer,
13371350
};
13381351
}
13391352

@@ -1356,6 +1369,8 @@ public ConnectionStatus GetStatus()
13561369
BytesInWritePipe = -1,
13571370
ReadStatus = _readStatus,
13581371
WriteStatus = _writeStatus,
1372+
BytesLastResult = bytesLastResult,
1373+
BytesInBuffer = bytesInBuffer,
13591374
};
13601375
}
13611376

@@ -1702,6 +1717,7 @@ private async Task ReadFromPipe()
17021717
private int ProcessBuffer(ref ReadOnlySequence<byte> buffer)
17031718
{
17041719
int messageCount = 0;
1720+
bytesInBuffer = buffer.Length;
17051721

17061722
while (!buffer.IsEmpty)
17071723
{
@@ -1718,6 +1734,10 @@ private int ProcessBuffer(ref ReadOnlySequence<byte> buffer)
17181734
Trace(result.ToString());
17191735
_readStatus = ReadStatus.MatchResult;
17201736
MatchResult(result);
1737+
1738+
// Track the last result size *after* processing for the *next* error message
1739+
bytesInBuffer = buffer.Length;
1740+
bytesLastResult = result.Payload.Length;
17211741
}
17221742
else
17231743
{

tests/StackExchange.Redis.Tests/AsyncTests.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public async Task AsyncTimeoutIsNoticed()
4848
using var conn = Create(syncTimeout: 1000);
4949
var opt = ConfigurationOptions.Parse(conn.Configuration);
5050
if (!Debugger.IsAttached)
51-
{ // we max the timeouts if a degugger is detected
51+
{ // we max the timeouts if a debugger is detected
5252
Assert.Equal(1000, opt.AsyncTimeout);
5353
}
5454

@@ -65,14 +65,20 @@ public async Task AsyncTimeoutIsNoticed()
6565
var ex = await Assert.ThrowsAsync<RedisTimeoutException>(async () =>
6666
{
6767
await db.StringGetAsync(key).ForAwait(); // but *subsequent* operations are paused
68-
ms.Stop();
68+
ms.Stop();
6969
Writer.WriteLine($"Unexpectedly succeeded after {ms.ElapsedMilliseconds}ms");
7070
}).ForAwait();
7171
ms.Stop();
7272
Writer.WriteLine($"Timed out after {ms.ElapsedMilliseconds}ms");
7373

74+
Writer.WriteLine("Exception message: " + ex.Message);
7475
Assert.Contains("Timeout awaiting response", ex.Message);
75-
Writer.WriteLine(ex.Message);
76+
// Ensure we are including the last payload size
77+
Assert.Contains("last-in:", ex.Message);
78+
Assert.DoesNotContain("last-in: 0", ex.Message);
79+
Assert.NotNull(ex.Data["Redis-Last-Result-Bytes"]);
80+
81+
Assert.Contains("cur-in:", ex.Message);
7682

7783
string status = conn.GetStatus();
7884
Writer.WriteLine(status);

tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,11 @@ public void TimeoutException()
112112
var ex = Assert.IsType<RedisTimeoutException>(rawEx);
113113
Writer.WriteLine("Exception: " + ex.Message);
114114

115-
// Example format: "Test Timeout, command=PING, inst: 0, qu: 0, qs: 0, aw: False, in: 0, in-pipe: 0, out-pipe: 0, serverEndpoint: 127.0.0.1:6379, mgr: 10 of 10 available, clientName: TimeoutException, IOCP: (Busy=0,Free=1000,Min=8,Max=1000), WORKER: (Busy=2,Free=2045,Min=8,Max=2047), v: 2.1.0 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts)";
115+
// Example format: "Test Timeout, command=PING, inst: 0, qu: 0, qs: 0, aw: False, in: 0, in-pipe: 0, out-pipe: 0, last-in: 0, cur-in: 0, serverEndpoint: 127.0.0.1:6379, mgr: 10 of 10 available, clientName: TimeoutException, IOCP: (Busy=0,Free=1000,Min=8,Max=1000), WORKER: (Busy=2,Free=2045,Min=8,Max=2047), v: 2.1.0 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts)";
116116
Assert.StartsWith("Test Timeout, command=PING", ex.Message);
117117
Assert.Contains("clientName: " + nameof(TimeoutException), ex.Message);
118118
// Ensure our pipe numbers are in place
119-
Assert.Contains("inst: 0, qu: 0, qs: 0, aw: False, bw: Inactive, in: 0, in-pipe: 0, out-pipe: 0", ex.Message);
119+
Assert.Contains("inst: 0, qu: 0, qs: 0, aw: False, bw: Inactive, in: 0, in-pipe: 0, out-pipe: 0, last-in: 0, cur-in: 0", ex.Message);
120120
Assert.Contains("mc: 1/1/0", ex.Message);
121121
Assert.Contains("serverEndpoint: " + server.EndPoint, ex.Message);
122122
Assert.Contains("IOCP: ", ex.Message);
@@ -183,19 +183,19 @@ public void NoConnectionException(bool abortOnConnect, int connCount, int comple
183183
var ex = Assert.IsType<RedisConnectionException>(rawEx);
184184
Writer.WriteLine("Exception: " + ex.Message);
185185

186-
// Example format: "Exception: No connection is active/available to service this operation: PING, inst: 0, qu: 0, qs: 0, aw: False, in: 0, in-pipe: 0, out-pipe: 0, serverEndpoint: 127.0.0.1:6379, mc: 1/1/0, mgr: 10 of 10 available, clientName: NoConnectionException, IOCP: (Busy=0,Free=1000,Min=8,Max=1000), WORKER: (Busy=2,Free=2045,Min=8,Max=2047), Local-CPU: 100%, v: 2.1.0.5";
186+
// Example format: "Exception: No connection is active/available to service this operation: PING, inst: 0, qu: 0, qs: 0, aw: False, in: 0, in-pipe: 0, out-pipe: 0, last-in: 0, cur-in: 0, serverEndpoint: 127.0.0.1:6379, mc: 1/1/0, mgr: 10 of 10 available, clientName: NoConnectionException, IOCP: (Busy=0,Free=1000,Min=8,Max=1000), WORKER: (Busy=2,Free=2045,Min=8,Max=2047), Local-CPU: 100%, v: 2.1.0.5";
187187
Assert.StartsWith(messageStart, ex.Message);
188188

189189
// Ensure our pipe numbers are in place if they should be
190190
if (hasDetail)
191191
{
192-
Assert.Contains("inst: 0, qu: 0, qs: 0, aw: False, bw: Inactive, in: 0, in-pipe: 0, out-pipe: 0", ex.Message);
192+
Assert.Contains("inst: 0, qu: 0, qs: 0, aw: False, bw: Inactive, in: 0, in-pipe: 0, out-pipe: 0, last-in: 0, cur-in: 0", ex.Message);
193193
Assert.Contains($"mc: {connCount}/{completeCount}/0", ex.Message);
194194
Assert.Contains("serverEndpoint: " + server.EndPoint.ToString()?.Replace("Unspecified/", ""), ex.Message);
195195
}
196196
else
197197
{
198-
Assert.DoesNotContain("inst: 0, qu: 0, qs: 0, aw: False, bw: Inactive, in: 0, in-pipe: 0, out-pipe: 0", ex.Message);
198+
Assert.DoesNotContain("inst: 0, qu: 0, qs: 0, aw: False, bw: Inactive, in: 0, in-pipe: 0, out-pipe: 0, last-in: 0, cur-in: 0", ex.Message);
199199
Assert.DoesNotContain($"mc: {connCount}/{completeCount}/0", ex.Message);
200200
Assert.DoesNotContain("serverEndpoint: " + server.EndPoint.ToString()?.Replace("Unspecified/", ""), ex.Message);
201201
}

0 commit comments

Comments
 (0)