Skip to content

Commit 3cb414a

Browse files
authored
Dylan/event source (#11516)
* Queuing Middleware now supports EventSources. Visibility for: items in queue, duration spent in queue, number of rejected requests.
1 parent 81b757a commit 3cb414a

15 files changed

+433
-122
lines changed

src/Hosting/Hosting/test/Internal/HostingEventSourceTests.cs

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System.Collections.Generic;
66
using System.Diagnostics.Tracing;
77
using System.Threading;
8-
using System.Threading.Channels;
98
using System.Threading.Tasks;
109
using Microsoft.AspNetCore.Http;
1110
using Microsoft.AspNetCore.Internal;
@@ -185,7 +184,7 @@ public void UnhandledException()
185184
public async Task VerifyCountersFireWithCorrectValues()
186185
{
187186
// Arrange
188-
var eventListener = new CounterListener(new[] {
187+
var eventListener = new TestCounterListener(new[] {
189188
"requests-per-second",
190189
"total-requests",
191190
"current-requests",
@@ -207,6 +206,7 @@ public async Task VerifyCountersFireWithCorrectValues()
207206
{ "EventCounterIntervalSec", "1" }
208207
});
209208

209+
// Act & Assert
210210
hostingEventSource.RequestStart("GET", "/");
211211

212212
Assert.Equal(1, await totalRequestValues.FirstOrDefault(v => v == 1));
@@ -241,36 +241,5 @@ private static HostingEventSource GetHostingEventSource()
241241
{
242242
return new HostingEventSource(Guid.NewGuid().ToString());
243243
}
244-
245-
private class CounterListener : EventListener
246-
{
247-
private readonly Dictionary<string, Channel<double>> _counters = new Dictionary<string, Channel<double>>();
248-
249-
public CounterListener(string[] counterNames)
250-
{
251-
foreach (var item in counterNames)
252-
{
253-
_counters[item] = Channel.CreateUnbounded<double>();
254-
}
255-
}
256-
257-
public IAsyncEnumerable<double> GetCounterValues(string counterName, CancellationToken cancellationToken = default)
258-
{
259-
return _counters[counterName].Reader.ReadAllAsync(cancellationToken);
260-
}
261-
262-
protected override void OnEventWritten(EventWrittenEventArgs eventData)
263-
{
264-
if (eventData.EventName == "EventCounters")
265-
{
266-
var payload = (IDictionary<string, object>)eventData.Payload[0];
267-
var counter = (string)payload["Name"];
268-
payload.TryGetValue("Increment", out var increment);
269-
payload.TryGetValue("Mean", out var mean);
270-
var writer = _counters[counter].Writer;
271-
writer.TryWrite((double)(increment ?? mean));
272-
}
273-
}
274-
}
275244
}
276245
}

src/Hosting/Hosting/test/Microsoft.AspNetCore.Hosting.Tests.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>netcoreapp3.0</TargetFramework>
@@ -7,6 +7,7 @@
77
<ItemGroup>
88
<Compile Include="$(SharedSourceRoot)test\SkipOnHelixAttribute.cs" />
99
<Compile Include="$(SharedSourceRoot)EventSource.Testing\TestEventListener.cs" />
10+
<Compile Include="$(SharedSourceRoot)EventSource.Testing\TestCounterListener.cs" />
1011
<Content Include="testroot\**\*" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
1112
<Content Include="Microsoft.AspNetCore.Hosting.StaticWebAssets.xml" CopyToOutputDirectory="PreserveNewest" />
1213
</ItemGroup>

src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
99
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
1010
<Reference Include="Microsoft.Extensions.Options" />
11+
<Reference Include="Microsoft.Extensions.ValueStopwatch.Sources" />
1112
</ItemGroup>
1213
</Project>

src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.netcoreapp3.0.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,9 @@ public static partial class RequestThrottlingExtensions
1010
}
1111
namespace Microsoft.AspNetCore.RequestThrottling
1212
{
13-
public partial interface IQueuePolicy
14-
{
15-
void OnExit();
16-
System.Threading.Tasks.Task<bool> TryEnterAsync();
17-
}
1813
public partial class RequestThrottlingMiddleware
1914
{
20-
public RequestThrottlingMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.AspNetCore.RequestThrottling.IQueuePolicy queue, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.RequestThrottling.RequestThrottlingOptions> options) { }
21-
public int QueuedRequestCount { get { throw null; } }
15+
public RequestThrottlingMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.AspNetCore.RequestThrottling.QueuePolicies.IQueuePolicy queue, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.RequestThrottling.RequestThrottlingOptions> options) { }
2216
[System.Diagnostics.DebuggerStepThroughAttribute]
2317
public System.Threading.Tasks.Task Invoke(Microsoft.AspNetCore.Http.HttpContext context) { throw null; }
2418
}
@@ -30,6 +24,11 @@ public RequestThrottlingOptions() { }
3024
}
3125
namespace Microsoft.AspNetCore.RequestThrottling.QueuePolicies
3226
{
27+
public partial interface IQueuePolicy
28+
{
29+
void OnExit();
30+
System.Threading.Tasks.Task<bool> TryEnterAsync();
31+
}
3332
public partial class QueuePolicyOptions
3433
{
3534
public QueuePolicyOptions() { }

src/Middleware/RequestThrottling/sample/Startup.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ public class Startup
2222
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
2323
public void ConfigureServices(IServiceCollection services)
2424
{
25-
services.AddTailDropQueue((options) =>
25+
services.AddStackQueue((options) =>
2626
{
27-
options.MaxConcurrentRequests = Math.Max(1, _config.GetValue<int>("maxCores"));
27+
options.MaxConcurrentRequests = Math.Max(1, _config.GetValue<int>("maxConcurrent"));
2828
options.RequestQueueLimit = Math.Max(1, _config.GetValue<int>("maxQueue"));
2929
});
3030

src/Middleware/RequestThrottling/src/Microsoft.AspNetCore.RequestThrottling.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<Description>ASP.NET Core middleware for queuing incoming HTTP requests, to avoid threadpool starvation.</Description>
@@ -12,6 +12,7 @@
1212
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
1313
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
1414
<Reference Include="Microsoft.Extensions.Options" />
15+
<Reference Include="Microsoft.Extensions.ValueStopwatch.Sources" />
1516
</ItemGroup>
1617

1718
</Project>

src/Middleware/RequestThrottling/src/QueuePolicies/IQueuePolicy.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
using System;
55
using System.Threading.Tasks;
66

7-
namespace Microsoft.AspNetCore.RequestThrottling
7+
namespace Microsoft.AspNetCore.RequestThrottling.QueuePolicies
88
{
99
/// <summary>
1010
/// Queueing policies, meant to be used with the <see cref="RequestThrottlingMiddleware"></see>.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics.Tracing;
7+
using System.Text;
8+
using System.Threading;
9+
using Microsoft.Extensions.Internal;
10+
11+
namespace Microsoft.AspNetCore.RequestThrottling
12+
{
13+
internal sealed class RequestThrottlingEventSource : EventSource
14+
{
15+
public static readonly RequestThrottlingEventSource Log = new RequestThrottlingEventSource();
16+
private static QueueFrame CachedNonTimerResult = new QueueFrame(null, Log);
17+
18+
private PollingCounter _rejectedRequestsCounter;
19+
private PollingCounter _queueLengthCounter;
20+
private EventCounter _queueDuration;
21+
22+
private long _rejectedRequests;
23+
private int _queueLength;
24+
25+
internal RequestThrottlingEventSource()
26+
: base("Microsoft.AspNetCore.RequestThrottling")
27+
{
28+
}
29+
30+
// Used for testing
31+
internal RequestThrottlingEventSource(string eventSourceName)
32+
: base(eventSourceName)
33+
{
34+
}
35+
36+
[Event(1, Level = EventLevel.Warning)]
37+
public void RequestRejected()
38+
{
39+
Interlocked.Increment(ref _rejectedRequests);
40+
WriteEvent(1);
41+
}
42+
43+
[NonEvent]
44+
public void QueueSkipped()
45+
{
46+
if (IsEnabled())
47+
{
48+
_queueDuration.WriteMetric(0);
49+
}
50+
}
51+
52+
[NonEvent]
53+
public QueueFrame QueueTimer()
54+
{
55+
Interlocked.Increment(ref _queueLength);
56+
57+
if (IsEnabled())
58+
{
59+
return new QueueFrame(ValueStopwatch.StartNew(), this);
60+
}
61+
62+
return CachedNonTimerResult;
63+
}
64+
65+
internal struct QueueFrame : IDisposable
66+
{
67+
private ValueStopwatch? _timer;
68+
private RequestThrottlingEventSource _parent;
69+
70+
public QueueFrame(ValueStopwatch? timer, RequestThrottlingEventSource parent)
71+
{
72+
_timer = timer;
73+
_parent = parent;
74+
}
75+
76+
public void Dispose()
77+
{
78+
Interlocked.Decrement(ref _parent._queueLength);
79+
80+
if (_parent.IsEnabled() && _timer != null)
81+
{
82+
var duration = _timer.Value.GetElapsedTime().TotalMilliseconds;
83+
_parent._queueDuration.WriteMetric(duration);
84+
}
85+
}
86+
}
87+
88+
protected override void OnEventCommand(EventCommandEventArgs command)
89+
{
90+
if (command.Command == EventCommand.Enable)
91+
{
92+
_rejectedRequestsCounter ??= new PollingCounter("requests-rejected", this, () => _rejectedRequests)
93+
{
94+
DisplayName = "Rejected Requests",
95+
};
96+
97+
_queueLengthCounter ??= new PollingCounter("queue-length", this, () => _queueLength)
98+
{
99+
DisplayName = "Queue Length",
100+
};
101+
102+
_queueDuration ??= new EventCounter("queue-duration", this)
103+
{
104+
DisplayName = "Average Time in Queue",
105+
};
106+
}
107+
}
108+
}
109+
}

src/Middleware/RequestThrottling/src/RequestThrottlingMiddleware.cs

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Threading;
65
using System.Threading.Tasks;
76
using Microsoft.AspNetCore.Http;
7+
using Microsoft.AspNetCore.RequestThrottling.QueuePolicies;
88
using Microsoft.Extensions.Logging;
99
using Microsoft.Extensions.Options;
1010

@@ -20,8 +20,6 @@ public class RequestThrottlingMiddleware
2020
private readonly RequestDelegate _onRejected;
2121
private readonly ILogger _logger;
2222

23-
private int _queuedRequests;
24-
2523
/// <summary>
2624
/// Creates a new <see cref="RequestThrottlingMiddleware"/>.
2725
/// </summary>
@@ -49,19 +47,21 @@ public RequestThrottlingMiddleware(RequestDelegate next, ILoggerFactory loggerFa
4947
/// <returns>A <see cref="Task"/> that completes when the request leaves.</returns>
5048
public async Task Invoke(HttpContext context)
5149
{
52-
Interlocked.Increment(ref _queuedRequests);
50+
var waitInQueueTask = _queuePolicy.TryEnterAsync();
5351

54-
var success = false;
55-
try
52+
if (waitInQueueTask.IsCompleted)
5653
{
57-
success = await _queuePolicy.TryEnterAsync();
54+
RequestThrottlingEventSource.Log.QueueSkipped();
5855
}
59-
finally
56+
else
6057
{
61-
Interlocked.Decrement(ref _queuedRequests);
58+
using (RequestThrottlingEventSource.Log.QueueTimer())
59+
{
60+
await waitInQueueTask;
61+
}
6262
}
6363

64-
if (success)
64+
if (waitInQueueTask.Result)
6565
{
6666
try
6767
{
@@ -74,20 +74,13 @@ public async Task Invoke(HttpContext context)
7474
}
7575
else
7676
{
77+
RequestThrottlingEventSource.Log.RequestRejected();
7778
RequestThrottlingLog.RequestRejectedQueueFull(_logger);
7879
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
7980
await _onRejected(context);
8081
}
8182
}
8283

83-
/// <summary>
84-
/// The total number of requests waiting within the middleware
85-
/// </summary>
86-
public int QueuedRequestCount
87-
{
88-
get => _queuedRequests;
89-
}
90-
9184
private static class RequestThrottlingLog
9285
{
9386
private static readonly Action<ILogger, int, Exception> _requestEnqueued =

src/Middleware/RequestThrottling/test/Microsoft.AspNetCore.RequestThrottling.Tests.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
</PropertyGroup>
66

77
<ItemGroup>
8-
<Compile Include="$(SharedSourceRoot)SyncPoint\SyncPoint.cs" />
8+
<Compile Include="$(SharedSourceRoot)EventSource.Testing\TestCounterListener.cs" />
9+
<Compile Include="$(SharedSourceRoot)EventSource.Testing\TestEventListener.cs" />
910
</ItemGroup>
1011

1112
<ItemGroup>

0 commit comments

Comments
 (0)