Implement new request trace identifier format

The format:
The trace identifier begins with connection ID and ends with a number that increments with each request per connection.

Example:
Connection ID = xyz
Request 1 = "xyz:00000001"
Request 2 = "xyz:00000002"
...
Request 15 = "xyz:0000000F"
Request 16 = "xyz:00000010"
This commit is contained in:
Nate McMaster 2017-04-27 12:02:32 -07:00
parent 3375cf0e28
commit 4dc7946cd8
8 changed files with 148 additions and 12 deletions

View File

@ -73,6 +73,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private string _requestId;
private int _remainingRequestHeadersBytesAllowed;
private int _requestHeadersParsed;
private uint _requestCount;
protected readonly long _keepAliveTicks;
private readonly long _requestHeadersTimeoutTicks;
@ -128,7 +129,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
// don't generate an ID until it is requested
if (_requestId == null)
{
_requestId = CorrelationIdGenerator.GetNextId();
_requestId = StringUtilities.ConcatAsHexSuffix(ConnectionId, ':', _requestCount);
}
return _requestId;
}
@ -388,6 +389,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_requestHeadersParsed = 0;
_responseBytesWritten = 0;
_requestCount++;
}
/// <summary>

View File

@ -44,7 +44,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
string key = new string('\0', keyLength);
fixed (char* keyBuffer = key)
{
if (!AsciiUtilities.TryGetAsciiString(pKeyBytes, keyBuffer, keyLength))
if (!StringUtilities.TryGetAsciiString(pKeyBytes, keyBuffer, keyLength))
{
throw BadHttpRequestException.GetException(RequestRejectionReason.InvalidCharactersInHeaderName);
}

View File

@ -119,7 +119,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{
// This version if AsciiUtilities returns null if there are any null (0 byte) characters
// in the string
if (!AsciiUtilities.TryGetAsciiString(buffer, output, span.Length))
if (!StringUtilities.TryGetAsciiString(buffer, output, span.Length))
{
throw new InvalidOperationException();
}

View File

@ -3,7 +3,7 @@
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{
internal class AsciiUtilities
internal class StringUtilities
{
public static unsafe bool TryGetAsciiString(byte* input, char* output, int count)
{
@ -75,5 +75,49 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
return isValid;
}
private static readonly string _encode16Chars = "0123456789ABCDEF";
/// <summary>
/// A faster version of String.Concat(<paramref name="str"/>, <paramref name="separator"/>, <paramref name="number"/>.ToString("X8"))
/// </summary>
/// <param name="str"></param>
/// <param name="separator"></param>
/// <param name="number"></param>
/// <returns></returns>
public static unsafe string ConcatAsHexSuffix(string str, char separator, uint number)
{
var length = 1 + 8;
if (str != null)
{
length += str.Length;
}
// stackalloc to allocate array on stack rather than heap
char* charBuffer = stackalloc char[length];
var i = 0;
if (str != null)
{
for (i = 0; i < str.Length; i++)
{
charBuffer[i] = str[i];
}
}
charBuffer[i] = separator;
charBuffer[i + 1] = _encode16Chars[(int)(number >> 28) & 0xF];
charBuffer[i + 2] = _encode16Chars[(int)(number >> 24) & 0xF];
charBuffer[i + 3] = _encode16Chars[(int)(number >> 20) & 0xF];
charBuffer[i + 4] = _encode16Chars[(int)(number >> 16) & 0xF];
charBuffer[i + 5] = _encode16Chars[(int)(number >> 12) & 0xF];
charBuffer[i + 6] = _encode16Chars[(int)(number >> 8) & 0xF];
charBuffer[i + 7] = _encode16Chars[(int)(number >> 4) & 0xF];
charBuffer[i + 8] = _encode16Chars[(int)number & 0xF];
// string ctor overload that takes char*
return new string(charBuffer, 0, length);
}
}
}

View File

@ -141,6 +141,31 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.NotEqual(nextId, secondId);
}
[Fact]
public void TraceIdentifierCountsRequestsPerFrame()
{
var connectionId = _frameContext.ConnectionId;
var feature = ((IFeatureCollection)_frame).Get<IHttpRequestIdentifierFeature>();
// Reset() is called once in the test ctor
var count = 1;
void Reset()
{
_frame.Reset();
count++;
}
var nextId = feature.TraceIdentifier;
Assert.Equal($"{connectionId}:00000001", nextId);
Reset();
var secondId = feature.TraceIdentifier;
Assert.Equal($"{connectionId}:00000002", secondId);
var big = 1_000_000;
while (big-- > 0) Reset();
Assert.Equal($"{connectionId}:{count:X8}", feature.TraceIdentifier);
}
[Fact]
public void TraceIdentifierGeneratesWhenNull()
{
@ -149,7 +174,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.NotNull(id);
Assert.Equal(id, _frame.TraceIdentifier);
_frame.TraceIdentifier = null;
_frame.Reset();
Assert.NotEqual(id, _frame.TraceIdentifier);
}

View File

@ -0,0 +1,31 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
public class StringUtilitiesTests
{
[Theory]
[InlineData(uint.MinValue)]
[InlineData(0xF)]
[InlineData(0xA)]
[InlineData(0xFF)]
[InlineData(0xFFC59)]
[InlineData(uint.MaxValue)]
public void ConvertsToHex(uint value)
{
var str = CorrelationIdGenerator.GetNextId();
Assert.Equal($"{str}:{value:X8}", StringUtilities.ConcatAsHexSuffix(str, ':', value));
}
[Fact]
public void HandlesNull()
{
uint value = 0x23BC0234;
Assert.Equal(":23BC0234", StringUtilities.ConcatAsHexSuffix(null, ':', value));
}
}
}

View File

@ -611,13 +611,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
[Fact]
public async Task TraceIdentifierIsUnique()
{
const int IdentifierLength = 13;
const int identifierLength = 22;
const int iterations = 10;
using (var server = new TestServer(async context =>
{
Assert.Equal(IdentifierLength, Encoding.ASCII.GetByteCount(context.TraceIdentifier));
context.Response.ContentLength = IdentifierLength;
Assert.Equal(identifierLength, Encoding.ASCII.GetByteCount(context.TraceIdentifier));
context.Response.ContentLength = identifierLength;
await context.Response.WriteAsync(context.TraceIdentifier);
}))
{
@ -635,7 +635,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
// requests on same connection
using (var connection = server.CreateConnection())
{
var buffer = new char[IdentifierLength];
var buffer = new char[identifierLength];
for (var i = 0; i < iterations; i++)
{
await connection.Send("GET / HTTP/1.1",
@ -645,12 +645,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
await connection.Receive($"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
$"Content-Length: {IdentifierLength}",
$"Content-Length: {identifierLength}",
"",
"").TimeoutAfter(TimeSpan.FromSeconds(10));
var read = await connection.Reader.ReadAsync(buffer, 0, IdentifierLength);
Assert.Equal(IdentifierLength, read);
var read = await connection.Reader.ReadAsync(buffer, 0, identifierLength);
Assert.Equal(identifierLength, read);
var id = new string(buffer, 0, read);
Assert.DoesNotContain(id, usedIds.ToArray());
usedIds.Add(id);

View File

@ -0,0 +1,34 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
{
[Config(typeof(CoreConfig))]
public class StringUtilitiesBenchmark
{
private const int Iterations = 500_000;
[Benchmark(Baseline = true, OperationsPerInvoke = Iterations)]
public void UintToString()
{
var connectionId = CorrelationIdGenerator.GetNextId();
for (uint i = 0; i < Iterations; i++)
{
var id = connectionId + ':' + i.ToString("X8");
}
}
[Benchmark(OperationsPerInvoke = Iterations)]
public void ConcatAsHexSuffix()
{
var connectionId = CorrelationIdGenerator.GetNextId();
for (uint i = 0; i < Iterations; i++)
{
var id = StringUtilities.ConcatAsHexSuffix(connectionId, ':', i);
}
}
}
}