Skip to content

Commit f8630ae

Browse files
committed
refactor: Replace pre-2000 queue-time floor with a max-queue-time cap
The pre-2000 floor only rejected timestamps that decoded before 2000, still accepting implausibly large queue times (e.g. an 11-year-stale start). A 10-minute cap on the computed delta strictly subsumes the floor: it rejects misdetected, stale, or garbage timestamps regardless of decoded date, while a real front-end queue (LB + network + host accept) never approaches it. Adds threshold comments to the unit auto-detection and cap boundary tests.
1 parent baf9386 commit f8630ae

2 files changed

Lines changed: 56 additions & 16 deletions

File tree

src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Helpers/QueueTimeHeaderParser.cs

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ namespace NewRelic.Agent.Extensions.Helpers;
1010
/// Parses request-queue-time headers set by upstream load balancers or ingress controllers
1111
/// (X-Request-Start, X-Queue-Start) and returns the time the request spent queued before
1212
/// reaching the application. Unit auto-detection handles ns/us/ms/s epoch timestamps.
13-
/// Values earlier than 2000-01-01 are rejected. Future timestamps (clock skew) yield null.
13+
/// Future timestamps (clock skew) yield null, as do queue times beyond a sanity cap
14+
/// (a misdetected unit or a stale/garbage timestamp produces an implausibly large delta).
1415
/// Parsing avoids regex and per-call allocations because it runs on every web request.
1516
/// </summary>
1617
public static class QueueTimeHeaderParser
@@ -19,14 +20,18 @@ public static class QueueTimeHeaderParser
1920
private const string HeaderQueueStart = "X-Queue-Start";
2021

2122
private static readonly DateTime _epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
22-
private static readonly DateTime _floor = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc);
23+
24+
// Upper bound on a plausible front-end queue (load balancer + network + host accept).
25+
// A genuine wait is seconds, not minutes; a larger delta means the timestamp was
26+
// misdetected, stale, or garbage, so we omit rather than report a bogus queue time.
27+
private static readonly TimeSpan _maxQueueTime = TimeSpan.FromMinutes(10);
2328

2429
/// <summary>
2530
/// Reads X-Request-Start and X-Queue-Start headers via <paramref name="getHeader"/>,
2631
/// parses Unix-epoch timestamps (auto-detects ns/us/ms/s by magnitude), and returns
2732
/// the queue time as <c>nowUtc - earliestStartTime</c>. Returns null when no valid
28-
/// header is found, all candidates predate 2000-01-01, or the start time is in the
29-
/// future relative to <paramref name="nowUtc"/> (clock skew guard).
33+
/// header is found, the start time is in the future relative to <paramref name="nowUtc"/>
34+
/// (clock skew guard), or the computed queue time exceeds the sanity cap.
3035
/// </summary>
3136
/// <param name="getHeader">Delegate that returns a header value by name, or null/empty if absent.</param>
3237
/// <param name="nowUtc">The current UTC time used to compute elapsed queue time.</param>
@@ -38,7 +43,11 @@ public static class QueueTimeHeaderParser
3843
if (earliest == null || earliest.Value > nowUtc)
3944
return null;
4045

41-
return nowUtc - earliest.Value;
46+
var queueTime = nowUtc - earliest.Value;
47+
if (queueTime > _maxQueueTime)
48+
return null;
49+
50+
return queueTime;
4251
}
4352

4453
private static DateTime? SelectEarlier(DateTime? current, string raw)
@@ -76,15 +85,19 @@ public static class QueueTimeHeaderParser
7685
if (double.IsNaN(value) || double.IsInfinity(value))
7786
return null;
7887

88+
// Auto-detect the timestamp unit by magnitude and normalize to seconds. A current
89+
// Unix epoch is ~1.7e9 in seconds (10 digits), ~1.7e12 in ms (13), ~1.7e15 in us (16),
90+
// and ~1.7e18 in ns (19). Each threshold sits an order of magnitude below its bucket
91+
// so the correct unit wins; values below 1e12 are taken as already being in seconds.
7992
double seconds;
8093
if (value >= 1e18)
81-
seconds = value / 1e9;
94+
seconds = value / 1e9; // nanoseconds -> seconds
8295
else if (value >= 1e15)
83-
seconds = value / 1e6;
96+
seconds = value / 1e6; // microseconds -> seconds
8497
else if (value >= 1e12)
85-
seconds = value / 1e3;
98+
seconds = value / 1e3; // milliseconds -> seconds
8699
else
87-
seconds = value;
100+
seconds = value; // already seconds
88101

89102
DateTime startTime;
90103
try
@@ -96,9 +109,6 @@ public static class QueueTimeHeaderParser
96109
return null;
97110
}
98111

99-
if (startTime < _floor)
100-
return null;
101-
102112
return startTime;
103113
}
104114

tests/Agent/UnitTests/NewRelic.Agent.Extensions.Tests/Helpers/QueueTimeHeaderParserTests.cs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,20 +143,50 @@ public void OnlyQueueStart_Present_StillWorks()
143143
}
144144

145145
[Test]
146-
public void Pre2000Timestamp_ReturnsNull()
146+
public void StaleTimestamp_BeyondMaxQueueTime_ReturnsNull()
147147
{
148-
// 1999-12-31 in milliseconds
149-
var pre2000 = new DateTime(1999, 12, 31, 0, 0, 0, DateTimeKind.Utc);
148+
// A valid but very old timestamp (e.g. from a misdetected unit or garbage) decodes to
149+
// a real date but an implausibly large queue time, which the sanity cap rejects.
150+
var stale = new DateTime(1999, 12, 31, 0, 0, 0, DateTimeKind.Utc);
150151
var headers = new Dictionary<string, string>
151152
{
152-
["X-Request-Start"] = $"t={ToMs(pre2000)}"
153+
["X-Request-Start"] = $"t={ToMs(stale)}"
153154
};
154155

155156
var result = QueueTimeHeaderParser.TryGetQueueTime(MakeGetter(headers), NowUtc);
156157

157158
Assert.That(result, Is.Null);
158159
}
159160

161+
[Test]
162+
public void QueueTime_JustOverCap_ReturnsNull()
163+
{
164+
var startTime = NowUtc.AddMinutes(-11); // cap is 10 minutes
165+
var headers = new Dictionary<string, string>
166+
{
167+
["X-Request-Start"] = $"t={ToMs(startTime)}"
168+
};
169+
170+
var result = QueueTimeHeaderParser.TryGetQueueTime(MakeGetter(headers), NowUtc);
171+
172+
Assert.That(result, Is.Null);
173+
}
174+
175+
[Test]
176+
public void QueueTime_JustUnderCap_ReturnsValue()
177+
{
178+
var startTime = NowUtc.AddMinutes(-9); // cap is 10 minutes
179+
var headers = new Dictionary<string, string>
180+
{
181+
["X-Request-Start"] = $"t={ToMs(startTime)}"
182+
};
183+
184+
var result = QueueTimeHeaderParser.TryGetQueueTime(MakeGetter(headers), NowUtc);
185+
186+
Assert.That(result, Is.Not.Null);
187+
Assert.That(result.Value.TotalMinutes, Is.EqualTo(9.0).Within(0.01));
188+
}
189+
160190
[Test]
161191
public void FutureTimestamp_ReturnsNull()
162192
{

0 commit comments

Comments
 (0)