diff --git a/src/Hosting/Abstractions/src/Microsoft.AspNetCore.Hosting.Abstractions.csproj b/src/Hosting/Abstractions/src/Microsoft.AspNetCore.Hosting.Abstractions.csproj index 4a53a07a61b4..9b5d4268cb21 100644 --- a/src/Hosting/Abstractions/src/Microsoft.AspNetCore.Hosting.Abstractions.csproj +++ b/src/Hosting/Abstractions/src/Microsoft.AspNetCore.Hosting.Abstractions.csproj @@ -11,28 +11,6 @@ <IsTrimmable>true</IsTrimmable> </PropertyGroup> - <ItemGroup> - <Compile Include="$(SharedSourceRoot)Metrics\**\*.cs" LinkBase="Metrics" /> - </ItemGroup> - - <!-- Temporary hack to make prototype Metrics DI integration types available --> - <!-- TODO: Remove when Metrics DI intergration package is available https://github.com/dotnet/aspnetcore/issues/47618 --> - <ItemGroup> - <InternalsVisibleTo Include="Microsoft.AspNetCore.Hosting" /> - <InternalsVisibleTo Include="Microsoft.AspNetCore.Hosting.Tests" /> - <InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Core" /> - <InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Core.Tests" /> - <InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Tests" /> - <InternalsVisibleTo Include="InMemory.FunctionalTests" /> - <InternalsVisibleTo Include="Sockets.BindTests" /> - <InternalsVisibleTo Include="Sockets.FunctionalTests" /> - <InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks" /> - <InternalsVisibleTo Include="Microsoft.AspNetCore.Http.Connections" /> - <InternalsVisibleTo Include="Microsoft.AspNetCore.Http.Connections.Tests" /> - <InternalsVisibleTo Include="Microsoft.AspNetCore.SignalR" /> - <InternalsVisibleTo Include="Microsoft.AspNetCore.Diagnostics.Tests" /> - </ItemGroup> - <ItemGroup> <Reference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" /> <Reference Include="Microsoft.AspNetCore.Http.Abstractions" /> diff --git a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj index ff0c54dadae1..ccdbcde598a7 100644 --- a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj +++ b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj @@ -53,6 +53,29 @@ Microsoft.AspNetCore.Http.HttpResponse</Description> </Compile> </ItemGroup> + <!-- Temporary hack to make prototype Metrics DI integration types available --> + <!-- TODO: Remove when Metrics DI intergration package is available https://github.com/dotnet/aspnetcore/issues/47618 --> + <ItemGroup> + <Compile Include="$(SharedSourceRoot)Metrics\**\*.cs" LinkBase="Metrics" /> + </ItemGroup> + <ItemGroup> + <InternalsVisibleTo Include="Microsoft.AspNetCore.Hosting" /> + <InternalsVisibleTo Include="Microsoft.AspNetCore.Hosting.Tests" /> + <InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Core" /> + <InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Core.Tests" /> + <InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Tests" /> + <InternalsVisibleTo Include="InMemory.FunctionalTests" /> + <InternalsVisibleTo Include="Sockets.BindTests" /> + <InternalsVisibleTo Include="Sockets.FunctionalTests" /> + <InternalsVisibleTo Include="Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks" /> + <InternalsVisibleTo Include="Microsoft.AspNetCore.Http.Connections" /> + <InternalsVisibleTo Include="Microsoft.AspNetCore.Http.Connections.Tests" /> + <InternalsVisibleTo Include="Microsoft.AspNetCore.SignalR" /> + <InternalsVisibleTo Include="Microsoft.AspNetCore.Diagnostics.Tests" /> + <InternalsVisibleTo Include="Microsoft.AspNetCore.RateLimiting" /> + <InternalsVisibleTo Include="Microsoft.AspNetCore.RateLimiting.Tests" /> + </ItemGroup> + <ItemGroup> <InternalsVisibleTo Include="Microsoft.AspNetCore.Http.Abstractions.Tests" /> </ItemGroup> diff --git a/src/Middleware/RateLimiting/src/LeaseContext.cs b/src/Middleware/RateLimiting/src/LeaseContext.cs index 1b78c3f8bf85..98068291bf34 100644 --- a/src/Middleware/RateLimiting/src/LeaseContext.cs +++ b/src/Middleware/RateLimiting/src/LeaseContext.cs @@ -22,4 +22,4 @@ internal enum RequestRejectionReason EndpointLimiter, GlobalLimiter, RequestCanceled -} \ No newline at end of file +} diff --git a/src/Middleware/RateLimiting/src/MetricsContext.cs b/src/Middleware/RateLimiting/src/MetricsContext.cs new file mode 100644 index 000000000000..36995e5a53a2 --- /dev/null +++ b/src/Middleware/RateLimiting/src/MetricsContext.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.RateLimiting; + +internal readonly struct MetricsContext +{ + public readonly string? PolicyName; + public readonly string? Method; + public readonly string? Route; + public readonly bool CurrentLeaseRequestsCounterEnabled; + public readonly bool CurrentRequestsQueuedCounterEnabled; + + public MetricsContext(string? policyName, string? method, string? route, bool currentLeaseRequestsCounterEnabled, bool currentRequestsQueuedCounterEnabled) + { + PolicyName = policyName; + Method = method; + Route = route; + CurrentLeaseRequestsCounterEnabled = currentLeaseRequestsCounterEnabled; + CurrentRequestsQueuedCounterEnabled = currentRequestsQueuedCounterEnabled; + } +} diff --git a/src/Middleware/RateLimiting/src/RateLimiterApplicationBuilderExtensions.cs b/src/Middleware/RateLimiting/src/RateLimiterApplicationBuilderExtensions.cs index cd1ec7b82604..360c3baafb3a 100644 --- a/src/Middleware/RateLimiting/src/RateLimiterApplicationBuilderExtensions.cs +++ b/src/Middleware/RateLimiting/src/RateLimiterApplicationBuilderExtensions.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.RateLimiting; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Resources = Microsoft.AspNetCore.RateLimiting.Resources; namespace Microsoft.AspNetCore.Builder; @@ -20,6 +22,8 @@ public static IApplicationBuilder UseRateLimiter(this IApplicationBuilder app) { ArgumentNullException.ThrowIfNull(app); + VerifyServicesAreRegistered(app); + return app.UseMiddleware<RateLimitingMiddleware>(); } @@ -34,6 +38,19 @@ public static IApplicationBuilder UseRateLimiter(this IApplicationBuilder app, R ArgumentNullException.ThrowIfNull(app); ArgumentNullException.ThrowIfNull(options); + VerifyServicesAreRegistered(app); + return app.UseMiddleware<RateLimitingMiddleware>(Options.Create(options)); } + + private static void VerifyServicesAreRegistered(IApplicationBuilder app) + { + var serviceProviderIsService = app.ApplicationServices.GetService<IServiceProviderIsService>(); + if (serviceProviderIsService != null && !serviceProviderIsService.IsService(typeof(RateLimitingMetrics))) + { + throw new InvalidOperationException(Resources.FormatUnableToFindServices( + nameof(IServiceCollection), + nameof(RateLimiterServiceCollectionExtensions.AddRateLimiter))); + } + } } diff --git a/src/Middleware/RateLimiting/src/RateLimiterServiceCollectionExtensions.cs b/src/Middleware/RateLimiting/src/RateLimiterServiceCollectionExtensions.cs index ac3c6718000f..09f6f7ba7c5c 100644 --- a/src/Middleware/RateLimiting/src/RateLimiterServiceCollectionExtensions.cs +++ b/src/Middleware/RateLimiting/src/RateLimiterServiceCollectionExtensions.cs @@ -22,6 +22,8 @@ public static IServiceCollection AddRateLimiter(this IServiceCollection services ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(configureOptions); + services.AddMetrics(); + services.AddSingleton<RateLimitingMetrics>(); services.Configure(configureOptions); return services; } diff --git a/src/Middleware/RateLimiting/src/RateLimitingMetrics.cs b/src/Middleware/RateLimiting/src/RateLimitingMetrics.cs new file mode 100644 index 000000000000..cf7bbb533e7a --- /dev/null +++ b/src/Middleware/RateLimiting/src/RateLimitingMetrics.cs @@ -0,0 +1,177 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.Metrics; + +namespace Microsoft.AspNetCore.RateLimiting; + +internal sealed class RateLimitingMetrics : IDisposable +{ + public const string MeterName = "Microsoft.AspNetCore.RateLimiting"; + + private readonly Meter _meter; + private readonly UpDownCounter<long> _currentLeasedRequestsCounter; + private readonly Histogram<double> _leasedRequestDurationCounter; + private readonly UpDownCounter<long> _currentQueuedRequestsCounter; + private readonly Histogram<double> _queuedRequestDurationCounter; + private readonly Counter<long> _leaseFailedRequestsCounter; + + public RateLimitingMetrics(IMeterFactory meterFactory) + { + _meter = meterFactory.CreateMeter(MeterName); + + _currentLeasedRequestsCounter = _meter.CreateUpDownCounter<long>( + "current-leased-requests", + description: "Number of HTTP requests that are currently active on the server that hold a rate limiting lease."); + + _leasedRequestDurationCounter = _meter.CreateHistogram<double>( + "leased-request-duration", + unit: "s", + description: "The duration of rate limiting leases held by HTTP requests on the server."); + + _currentQueuedRequestsCounter = _meter.CreateUpDownCounter<long>( + "current-queued-requests", + description: "Number of HTTP requests that are currently queued, waiting to acquire a rate limiting lease."); + + _queuedRequestDurationCounter = _meter.CreateHistogram<double>( + "queued-request-duration", + unit: "s", + description: "The duration of HTTP requests in a queue, waiting to acquire a rate limiting lease."); + + _leaseFailedRequestsCounter = _meter.CreateCounter<long>( + "lease-failed-requests", + description: "Number of HTTP requests that failed to acquire a rate limiting lease. Requests could be rejected by global or endpoint rate limiting policies. Or the request could be canceled while waiting for the lease."); + } + + public bool CurrentLeasedRequestsCounterEnabled => _currentLeasedRequestsCounter.Enabled; + public bool CurrentQueuedRequestsCounterEnabled => _currentQueuedRequestsCounter.Enabled; + + public void LeaseFailed(in MetricsContext metricsContext, RequestRejectionReason reason) + { + if (_leaseFailedRequestsCounter.Enabled) + { + LeaseFailedCore(metricsContext, reason); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void LeaseFailedCore(in MetricsContext metricsContext, RequestRejectionReason reason) + { + var tags = new TagList(); + InitializeRateLimitingTags(ref tags, metricsContext); + tags.Add("reason", reason.ToString()); + _leaseFailedRequestsCounter.Add(1, tags); + } + + public void LeaseStart(in MetricsContext metricsContext) + { + if (metricsContext.CurrentLeaseRequestsCounterEnabled) + { + LeaseStartCore(metricsContext); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public void LeaseStartCore(in MetricsContext metricsContext) + { + var tags = new TagList(); + InitializeRateLimitingTags(ref tags, metricsContext); + _currentLeasedRequestsCounter.Add(1, tags); + } + + public void LeaseEnd(in MetricsContext metricsContext, long startTimestamp, long currentTimestamp) + { + if (metricsContext.CurrentLeaseRequestsCounterEnabled || _leasedRequestDurationCounter.Enabled) + { + LeaseEndCore(metricsContext, startTimestamp, currentTimestamp); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void LeaseEndCore(in MetricsContext metricsContext, long startTimestamp, long currentTimestamp) + { + var tags = new TagList(); + InitializeRateLimitingTags(ref tags, metricsContext); + + if (metricsContext.CurrentLeaseRequestsCounterEnabled) + { + _currentLeasedRequestsCounter.Add(-1, tags); + } + + if (_leasedRequestDurationCounter.Enabled) + { + var duration = Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp); + _leasedRequestDurationCounter.Record(duration.TotalSeconds, tags); + } + } + + public void QueueStart(in MetricsContext metricsContext) + { + if (metricsContext.CurrentRequestsQueuedCounterEnabled) + { + QueueStartCore(metricsContext); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void QueueStartCore(in MetricsContext metricsContext) + { + var tags = new TagList(); + InitializeRateLimitingTags(ref tags, metricsContext); + _currentQueuedRequestsCounter.Add(1, tags); + } + + public void QueueEnd(in MetricsContext metricsContext, RequestRejectionReason? reason, long startTimestamp, long currentTimestamp) + { + if (metricsContext.CurrentRequestsQueuedCounterEnabled || _queuedRequestDurationCounter.Enabled) + { + QueueEndCore(metricsContext, reason, startTimestamp, currentTimestamp); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void QueueEndCore(in MetricsContext metricsContext, RequestRejectionReason? reason, long startTimestamp, long currentTimestamp) + { + var tags = new TagList(); + InitializeRateLimitingTags(ref tags, metricsContext); + + if (metricsContext.CurrentRequestsQueuedCounterEnabled) + { + _currentQueuedRequestsCounter.Add(-1, tags); + } + + if (_queuedRequestDurationCounter.Enabled) + { + if (reason != null) + { + tags.Add("reason", reason.Value.ToString()); + } + var duration = Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp); + _queuedRequestDurationCounter.Record(duration.TotalSeconds, tags); + } + } + + public void Dispose() + { + _meter.Dispose(); + } + + private static void InitializeRateLimitingTags(ref TagList tags, in MetricsContext metricsContext) + { + if (metricsContext.PolicyName is not null) + { + tags.Add("policy", metricsContext.PolicyName); + } + if (metricsContext.Method is not null) + { + tags.Add("method", metricsContext.Method); + } + if (metricsContext.Route is not null) + { + tags.Add("route", metricsContext.Route); + } + } +} diff --git a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs index d743f77feea6..2e494b8c5449 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs @@ -1,8 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Threading.RateLimiting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -16,6 +18,7 @@ internal sealed partial class RateLimitingMiddleware private readonly RequestDelegate _next; private readonly Func<OnRejectedContext, CancellationToken, ValueTask>? _defaultOnRejected; private readonly ILogger _logger; + private readonly RateLimitingMetrics _metrics; private readonly PartitionedRateLimiter<HttpContext>? _globalLimiter; private readonly PartitionedRateLimiter<HttpContext> _endpointLimiter; private readonly int _rejectionStatusCode; @@ -29,14 +32,17 @@ internal sealed partial class RateLimitingMiddleware /// <param name="logger">The <see cref="ILogger"/> used for logging.</param> /// <param name="options">The options for the middleware.</param> /// <param name="serviceProvider">The service provider.</param> - public RateLimitingMiddleware(RequestDelegate next, ILogger<RateLimitingMiddleware> logger, IOptions<RateLimiterOptions> options, IServiceProvider serviceProvider) + /// <param name="metrics">The rate limiting metrics.</param> + public RateLimitingMiddleware(RequestDelegate next, ILogger<RateLimitingMiddleware> logger, IOptions<RateLimiterOptions> options, IServiceProvider serviceProvider, RateLimitingMetrics metrics) { ArgumentNullException.ThrowIfNull(next); ArgumentNullException.ThrowIfNull(logger); ArgumentNullException.ThrowIfNull(serviceProvider); + ArgumentNullException.ThrowIfNull(metrics); _next = next; _logger = logger; + _metrics = metrics; _defaultOnRejected = options.Value.OnRejected; _rejectionStatusCode = options.Value.RejectionStatusCode; _policyMap = new Dictionary<string, DefaultRateLimiterPolicy>(options.Value.PolicyMap); @@ -49,7 +55,6 @@ public RateLimitingMiddleware(RequestDelegate next, ILogger<RateLimitingMiddlewa _globalLimiter = options.Value.GlobalLimiter; _endpointLimiter = CreateEndpointLimiter(); - } // TODO - EventSource? @@ -72,18 +77,47 @@ public Task Invoke(HttpContext context) { return _next(context); } - return InvokeInternal(context, enableRateLimitingAttribute); + + // Only include route and method if we have an endpoint with a route. + // A request always has a HTTP request, but it isn't useful unless combined with a route. + var route = endpoint?.Metadata.GetMetadata<IRouteDiagnosticsMetadata>()?.Route; + var method = route is not null ? context.Request.Method : null; + + return InvokeInternal(context, enableRateLimitingAttribute, method, route); } - private async Task InvokeInternal(HttpContext context, EnableRateLimitingAttribute? enableRateLimitingAttribute) + private async Task InvokeInternal(HttpContext context, EnableRateLimitingAttribute? enableRateLimitingAttribute, string? method, string? route) { - using var leaseContext = await TryAcquireAsync(context); + var policyName = enableRateLimitingAttribute?.PolicyName; + + // Cache the up/down counter enabled state at the start of the middleware. + // This ensures that the state is consistent for the entire request. + // For example, if a meter listener starts after a request is queued, when the request exits the queue + // the requests queued counter won't go into a negative value. + var metricsContext = new MetricsContext(policyName, method, route, + _metrics.CurrentLeasedRequestsCounterEnabled, _metrics.CurrentQueuedRequestsCounterEnabled); + + using var leaseContext = await TryAcquireAsync(context, metricsContext); + if (leaseContext.Lease?.IsAcquired == true) { - await _next(context); + var startTimestamp = Stopwatch.GetTimestamp(); + var currentLeaseStart = _metrics.CurrentLeasedRequestsCounterEnabled; + try + { + + _metrics.LeaseStart(metricsContext); + await _next(context); + } + finally + { + _metrics.LeaseEnd(metricsContext, startTimestamp, Stopwatch.GetTimestamp()); + } } else { + _metrics.LeaseFailed(metricsContext, leaseContext.RequestRejectionReason!.Value); + // If the request was canceled, do not call OnRejected, just return. if (leaseContext.RequestRejectionReason == RequestRejectionReason.RequestCanceled) { @@ -107,7 +141,6 @@ private async Task InvokeInternal(HttpContext context, EnableRateLimitingAttribu } else { - var policyName = enableRateLimitingAttribute?.PolicyName; if (policyName is not null && _policyMap.TryGetValue(policyName, out policy) && policy.OnRejected is not null) { thisRequestOnRejected = policy.OnRejected; @@ -122,15 +155,32 @@ private async Task InvokeInternal(HttpContext context, EnableRateLimitingAttribu } } - private ValueTask<LeaseContext> TryAcquireAsync(HttpContext context) + private async ValueTask<LeaseContext> TryAcquireAsync(HttpContext context, MetricsContext metricsContext) { var leaseContext = CombinedAcquire(context); if (leaseContext.Lease?.IsAcquired == true) { - return ValueTask.FromResult(leaseContext); + return leaseContext; + } + + var waitTask = CombinedWaitAsync(context, context.RequestAborted); + // If the task returns immediately then the request wasn't queued. + if (waitTask.IsCompleted) + { + return await waitTask; } - return CombinedWaitAsync(context, context.RequestAborted); + var startTimestamp = Stopwatch.GetTimestamp(); + try + { + _metrics.QueueStart(metricsContext); + leaseContext = await waitTask; + return leaseContext; + } + finally + { + _metrics.QueueEnd(metricsContext, leaseContext.RequestRejectionReason, startTimestamp, Stopwatch.GetTimestamp()); + } } private LeaseContext CombinedAcquire(HttpContext context) @@ -253,4 +303,4 @@ private static partial class RateLimiterLog [LoggerMessage(3, LogLevel.Debug, "The request was canceled.", EventName = "RequestCanceled")] internal static partial void RequestCanceled(ILogger logger); } -} \ No newline at end of file +} diff --git a/src/Middleware/RateLimiting/src/Resources.resx b/src/Middleware/RateLimiting/src/Resources.resx new file mode 100644 index 000000000000..f50493b9d4a9 --- /dev/null +++ b/src/Middleware/RateLimiting/src/Resources.resx @@ -0,0 +1,123 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="UnableToFindServices" xml:space="preserve"> + <value>Unable to find the required services. Please add all the required services by calling '{0}.{1}' in the application startup code.</value> + </data> +</root> \ No newline at end of file diff --git a/src/Middleware/RateLimiting/test/Microsoft.AspNetCore.RateLimiting.Tests.csproj b/src/Middleware/RateLimiting/test/Microsoft.AspNetCore.RateLimiting.Tests.csproj index 9712d186644a..22890a28eae9 100644 --- a/src/Middleware/RateLimiting/test/Microsoft.AspNetCore.RateLimiting.Tests.csproj +++ b/src/Middleware/RateLimiting/test/Microsoft.AspNetCore.RateLimiting.Tests.csproj @@ -7,5 +7,8 @@ <ItemGroup> <Reference Include="Microsoft.AspNetCore.Http" /> <Reference Include="Microsoft.AspNetCore.RateLimiting" /> + <Reference Include="Microsoft.AspNetCore.Routing" /> + + <Compile Include="$(SharedSourceRoot)SyncPoint\SyncPoint.cs" Link="Internal\SyncPoint.cs" /> </ItemGroup> </Project> diff --git a/src/Middleware/RateLimiting/test/RateLimitingApplicationBuilderExtensionsTests.cs b/src/Middleware/RateLimiting/test/RateLimitingApplicationBuilderExtensionsTests.cs index 45afd2182a4d..55d4b0b3ab7b 100644 --- a/src/Middleware/RateLimiting/test/RateLimitingApplicationBuilderExtensionsTests.cs +++ b/src/Middleware/RateLimiting/test/RateLimitingApplicationBuilderExtensionsTests.cs @@ -10,7 +10,6 @@ namespace Microsoft.AspNetCore.RateLimiting; public class RateLimitingApplicationBuilderExtensionsTests : LoggedTest { - [Fact] public void UseRateLimiter_ThrowsOnNullAppBuilder() { @@ -24,6 +23,18 @@ public void UseRateLimiter_ThrowsOnNullOptions() Assert.Throws<ArgumentNullException>(() => appBuilder.UseRateLimiter(null)); } + [Fact] + public void UseRateLimiter_RequireServices() + { + var services = new ServiceCollection(); + var serviceProvider = services.BuildServiceProvider(); + var appBuilder = new ApplicationBuilder(serviceProvider); + + // Act + var ex = Assert.Throws<InvalidOperationException>(() => appBuilder.UseRateLimiter()); + Assert.Equal("Unable to find the required services. Please add all the required services by calling 'IServiceCollection.AddRateLimiter' in the application startup code.", ex.Message); + } + [Fact] public void UseRateLimiter_RespectsOptions() { @@ -34,7 +45,7 @@ public void UseRateLimiter_RespectsOptions() // These should not get used var services = new ServiceCollection(); - services.Configure<RateLimiterOptions>(options => + services.AddRateLimiter(options => { options.GlobalLimiter = new TestPartitionedRateLimiter<HttpContext>(new TestRateLimiter(false)); options.RejectionStatusCode = 404; diff --git a/src/Middleware/RateLimiting/test/RateLimitingMetricsTests.cs b/src/Middleware/RateLimiting/test/RateLimitingMetricsTests.cs new file mode 100644 index 000000000000..9f5f7388daf9 --- /dev/null +++ b/src/Middleware/RateLimiting/test/RateLimitingMetricsTests.cs @@ -0,0 +1,347 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.Metrics; +using System.Threading.RateLimiting; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Internal; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Metrics; +using Microsoft.Extensions.Options; +using Moq; + +namespace Microsoft.AspNetCore.RateLimiting; + +public class RateLimitingMetricsTests +{ + [Fact] + public async Task Metrics_Rejected() + { + // Arrange + var meterFactory = new TestMeterFactory(); + var meterRegistry = new TestMeterRegistry(meterFactory.Meters); + + var options = CreateOptionsAccessor(); + options.Value.GlobalLimiter = new TestPartitionedRateLimiter<HttpContext>(new TestRateLimiter(false)); + + var middleware = CreateTestRateLimitingMiddleware(options, meterFactory: meterFactory); + var meter = meterFactory.Meters.Single(); + + var context = new DefaultHttpContext(); + + using var leaseRequestDurationRecorder = new InstrumentRecorder<double>(meterRegistry, RateLimitingMetrics.MeterName, "leased-request-duration"); + using var currentLeaseRequestsRecorder = new InstrumentRecorder<long>(meterRegistry, RateLimitingMetrics.MeterName, "current-leased-requests"); + using var currentRequestsQueuedRecorder = new InstrumentRecorder<long>(meterRegistry, RateLimitingMetrics.MeterName, "current-queued-requests"); + using var queuedRequestDurationRecorder = new InstrumentRecorder<double>(meterRegistry, RateLimitingMetrics.MeterName, "queued-request-duration"); + using var leaseFailedRequestsRecorder = new InstrumentRecorder<long>(meterRegistry, RateLimitingMetrics.MeterName, "lease-failed-requests"); + + // Act + await middleware.Invoke(context).DefaultTimeout(); + + // Assert + Assert.Equal(StatusCodes.Status503ServiceUnavailable, context.Response.StatusCode); + + Assert.Empty(currentLeaseRequestsRecorder.GetMeasurements()); + Assert.Empty(leaseRequestDurationRecorder.GetMeasurements()); + Assert.Empty(currentRequestsQueuedRecorder.GetMeasurements()); + Assert.Empty(queuedRequestDurationRecorder.GetMeasurements()); + Assert.Collection(leaseFailedRequestsRecorder.GetMeasurements(), + m => + { + Assert.Equal(1, m.Value); + Assert.Equal("GlobalLimiter", (string)m.Tags.ToArray().Single(t => t.Key == "reason").Value); + }); + } + + [Fact] + public async Task Metrics_Success() + { + // Arrange + var syncPoint = new SyncPoint(); + + var meterFactory = new TestMeterFactory(); + var meterRegistry = new TestMeterRegistry(meterFactory.Meters); + + var options = CreateOptionsAccessor(); + options.Value.GlobalLimiter = new TestPartitionedRateLimiter<HttpContext>(new TestRateLimiter(true)); + + var middleware = CreateTestRateLimitingMiddleware( + options, + meterFactory: meterFactory, + next: async c => + { + await syncPoint.WaitToContinue(); + }); + var meter = meterFactory.Meters.Single(); + + var context = new DefaultHttpContext(); + context.Request.Method = "GET"; + + using var leaseRequestDurationRecorder = new InstrumentRecorder<double>(meterRegistry, RateLimitingMetrics.MeterName, "leased-request-duration"); + using var currentLeaseRequestsRecorder = new InstrumentRecorder<long>(meterRegistry, RateLimitingMetrics.MeterName, "current-leased-requests"); + using var currentRequestsQueuedRecorder = new InstrumentRecorder<long>(meterRegistry, RateLimitingMetrics.MeterName, "current-queued-requests"); + using var queuedRequestDurationRecorder = new InstrumentRecorder<double>(meterRegistry, RateLimitingMetrics.MeterName, "queued-request-duration"); + using var leaseFailedRequestsRecorder = new InstrumentRecorder<long>(meterRegistry, RateLimitingMetrics.MeterName, "lease-failed-requests"); + + // Act + var middlewareTask = middleware.Invoke(context); + + await syncPoint.WaitForSyncPoint().DefaultTimeout(); + + Assert.Collection(currentLeaseRequestsRecorder.GetMeasurements(), + m => AssertCounter(m, 1, null, null, null)); + Assert.Empty(leaseRequestDurationRecorder.GetMeasurements()); + + syncPoint.Continue(); + + await middlewareTask.DefaultTimeout(); + + // Assert + Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + + Assert.Collection(currentLeaseRequestsRecorder.GetMeasurements(), + m => AssertCounter(m, 1, null, null, null), + m => AssertCounter(m, -1, null, null, null)); + Assert.Collection(leaseRequestDurationRecorder.GetMeasurements(), + m => AssertDuration(m, null, null, null)); + Assert.Empty(currentRequestsQueuedRecorder.GetMeasurements()); + Assert.Empty(queuedRequestDurationRecorder.GetMeasurements()); + Assert.Empty(leaseFailedRequestsRecorder.GetMeasurements()); + } + + [Fact] + public async Task Metrics_ListenInMiddleOfRequest_CurrentLeasesNotDecreased() + { + // Arrange + var syncPoint = new SyncPoint(); + + var meterFactory = new TestMeterFactory(); + var meterRegistry = new TestMeterRegistry(meterFactory.Meters); + + var options = CreateOptionsAccessor(); + options.Value.GlobalLimiter = new TestPartitionedRateLimiter<HttpContext>(new TestRateLimiter(true)); + + var middleware = CreateTestRateLimitingMiddleware( + options, + meterFactory: meterFactory, + next: async c => + { + await syncPoint.WaitToContinue(); + }); + var meter = meterFactory.Meters.Single(); + + var context = new DefaultHttpContext(); + context.Request.Method = "GET"; + + // Act + var middlewareTask = middleware.Invoke(context); + + await syncPoint.WaitForSyncPoint().DefaultTimeout(); + + using var leaseRequestDurationRecorder = new InstrumentRecorder<double>(meterRegistry, RateLimitingMetrics.MeterName, "leased-request-duration"); + using var currentLeaseRequestsRecorder = new InstrumentRecorder<long>(meterRegistry, RateLimitingMetrics.MeterName, "current-leased-requests"); + using var currentRequestsQueuedRecorder = new InstrumentRecorder<long>(meterRegistry, RateLimitingMetrics.MeterName, "current-queued-requests"); + using var queuedRequestDurationRecorder = new InstrumentRecorder<double>(meterRegistry, RateLimitingMetrics.MeterName, "queued-request-duration"); + using var leaseFailedRequestsRecorder = new InstrumentRecorder<long>(meterRegistry, RateLimitingMetrics.MeterName, "lease-failed-requests"); + + syncPoint.Continue(); + + await middlewareTask.DefaultTimeout(); + + // Assert + Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + + Assert.Empty(currentLeaseRequestsRecorder.GetMeasurements()); + Assert.Collection(leaseRequestDurationRecorder.GetMeasurements(), + m => AssertDuration(m, null, null, null)); + } + + [Fact] + public async Task Metrics_Queued() + { + // Arrange + var syncPoint = new SyncPoint(); + + var meterFactory = new TestMeterFactory(); + var meterRegistry = new TestMeterRegistry(meterFactory.Meters); + + var services = new ServiceCollection(); + + services.AddRateLimiter(_ => _ + .AddConcurrencyLimiter(policyName: "concurrencyPolicy", options => + { + options.PermitLimit = 1; + options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; + options.QueueLimit = 1; + })); + var serviceProvider = services.BuildServiceProvider(); + + var middleware = CreateTestRateLimitingMiddleware( + serviceProvider.GetRequiredService<IOptions<RateLimiterOptions>>(), + meterFactory: meterFactory, + next: async c => + { + await syncPoint.WaitToContinue(); + }, + serviceProvider: serviceProvider); + var meter = meterFactory.Meters.Single(); + + var routeEndpointBuilder = new RouteEndpointBuilder(c => Task.CompletedTask, RoutePatternFactory.Parse("/"), 0); + routeEndpointBuilder.Metadata.Add(new EnableRateLimitingAttribute("concurrencyPolicy")); + var endpoint = routeEndpointBuilder.Build(); + + using var leaseRequestDurationRecorder = new InstrumentRecorder<double>(meterRegistry, RateLimitingMetrics.MeterName, "leased-request-duration"); + using var currentLeaseRequestsRecorder = new InstrumentRecorder<long>(meterRegistry, RateLimitingMetrics.MeterName, "current-leased-requests"); + using var currentRequestsQueuedRecorder = new InstrumentRecorder<long>(meterRegistry, RateLimitingMetrics.MeterName, "current-queued-requests"); + using var queuedRequestDurationRecorder = new InstrumentRecorder<double>(meterRegistry, RateLimitingMetrics.MeterName, "queued-request-duration"); + using var leaseFailedRequestsRecorder = new InstrumentRecorder<long>(meterRegistry, RateLimitingMetrics.MeterName, "lease-failed-requests"); + + // Act + var context1 = new DefaultHttpContext(); + context1.Request.Method = "GET"; + context1.SetEndpoint(endpoint); + var middlewareTask1 = middleware.Invoke(context1); + + // Wait for first request to reach server and block it. + await syncPoint.WaitForSyncPoint().DefaultTimeout(); + + var context2 = new DefaultHttpContext(); + context2.Request.Method = "GET"; + context2.SetEndpoint(endpoint); + var middlewareTask2 = middleware.Invoke(context1); + + // Assert second request is queued. + Assert.Collection(currentRequestsQueuedRecorder.GetMeasurements(), + m => AssertCounter(m, 1, "GET", "/", "concurrencyPolicy")); + Assert.Empty(queuedRequestDurationRecorder.GetMeasurements()); + + // Allow both requests to finish. + syncPoint.Continue(); + + await middlewareTask1.DefaultTimeout(); + await middlewareTask2.DefaultTimeout(); + + Assert.Collection(currentRequestsQueuedRecorder.GetMeasurements(), + m => AssertCounter(m, 1, "GET", "/", "concurrencyPolicy"), + m => AssertCounter(m, -1, "GET", "/", "concurrencyPolicy")); + Assert.Collection(queuedRequestDurationRecorder.GetMeasurements(), + m => AssertDuration(m, "GET", "/", "concurrencyPolicy")); + } + + [Fact] + public async Task Metrics_ListenInMiddleOfQueued_CurrentQueueNotDecreased() + { + // Arrange + var syncPoint = new SyncPoint(); + + var meterFactory = new TestMeterFactory(); + var meterRegistry = new TestMeterRegistry(meterFactory.Meters); + + var services = new ServiceCollection(); + + services.AddRateLimiter(_ => _ + .AddConcurrencyLimiter(policyName: "concurrencyPolicy", options => + { + options.PermitLimit = 1; + options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; + options.QueueLimit = 1; + })); + var serviceProvider = services.BuildServiceProvider(); + + var middleware = CreateTestRateLimitingMiddleware( + serviceProvider.GetRequiredService<IOptions<RateLimiterOptions>>(), + meterFactory: meterFactory, + next: async c => + { + await syncPoint.WaitToContinue(); + }, + serviceProvider: serviceProvider); + var meter = meterFactory.Meters.Single(); + + var routeEndpointBuilder = new RouteEndpointBuilder(c => Task.CompletedTask, RoutePatternFactory.Parse("/"), 0); + routeEndpointBuilder.Metadata.Add(new EnableRateLimitingAttribute("concurrencyPolicy")); + var endpoint = routeEndpointBuilder.Build(); + + // Act + var context1 = new DefaultHttpContext(); + context1.Request.Method = "GET"; + context1.SetEndpoint(endpoint); + var middlewareTask1 = middleware.Invoke(context1); + + // Wait for first request to reach server and block it. + await syncPoint.WaitForSyncPoint().DefaultTimeout(); + + var context2 = new DefaultHttpContext(); + context2.Request.Method = "GET"; + context2.SetEndpoint(endpoint); + var middlewareTask2 = middleware.Invoke(context1); + + // Start listening while the second request is queued. + + using var leaseRequestDurationRecorder = new InstrumentRecorder<double>(meterRegistry, RateLimitingMetrics.MeterName, "leased-request-duration"); + using var currentLeaseRequestsRecorder = new InstrumentRecorder<long>(meterRegistry, RateLimitingMetrics.MeterName, "current-leased-requests"); + using var currentRequestsQueuedRecorder = new InstrumentRecorder<long>(meterRegistry, RateLimitingMetrics.MeterName, "current-queued-requests"); + using var queuedRequestDurationRecorder = new InstrumentRecorder<double>(meterRegistry, RateLimitingMetrics.MeterName, "queued-request-duration"); + using var leaseFailedRequestsRecorder = new InstrumentRecorder<long>(meterRegistry, RateLimitingMetrics.MeterName, "lease-failed-requests"); + + Assert.Empty(currentRequestsQueuedRecorder.GetMeasurements()); + Assert.Empty(queuedRequestDurationRecorder.GetMeasurements()); + + // Allow both requests to finish. + syncPoint.Continue(); + + await middlewareTask1.DefaultTimeout(); + await middlewareTask2.DefaultTimeout(); + + Assert.Empty(currentRequestsQueuedRecorder.GetMeasurements()); + Assert.Collection(queuedRequestDurationRecorder.GetMeasurements(), + m => AssertDuration(m, "GET", "/", "concurrencyPolicy")); + } + + private static void AssertCounter(Measurement<long> measurement, long value, string method, string route, string policy) + { + Assert.Equal(value, measurement.Value); + AssertTag(measurement.Tags, "method", method); + AssertTag(measurement.Tags, "route", route); + AssertTag(measurement.Tags, "policy", policy); + } + + private static void AssertDuration(Measurement<double> measurement, string method, string route, string policy) + { + Assert.True(measurement.Value > 0); + AssertTag(measurement.Tags, "method", method); + AssertTag(measurement.Tags, "route", route); + AssertTag(measurement.Tags, "policy", policy); + } + + private static void AssertTag<T>(ReadOnlySpan<KeyValuePair<string, object>> tags, string tagName, T expected) + { + if (expected == null) + { + Assert.DoesNotContain(tags.ToArray(), t => t.Key == tagName); + } + else + { + Assert.Equal(expected, (T)tags.ToArray().Single(t => t.Key == tagName).Value); + } + } + + private RateLimitingMiddleware CreateTestRateLimitingMiddleware(IOptions<RateLimiterOptions> options, ILogger<RateLimitingMiddleware> logger = null, IServiceProvider serviceProvider = null, IMeterFactory meterFactory = null, RequestDelegate next = null) + { + next ??= c => Task.CompletedTask; + return new RateLimitingMiddleware( + next, + logger ?? new NullLoggerFactory().CreateLogger<RateLimitingMiddleware>(), + options, + serviceProvider ?? Mock.Of<IServiceProvider>(), + new RateLimitingMetrics(meterFactory ?? new TestMeterFactory())); + } + + private IOptions<RateLimiterOptions> CreateOptionsAccessor() => Options.Create(new RateLimiterOptions()); +} diff --git a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs index 24c6ccd93990..b64c7a258195 100644 --- a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs +++ b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.Metrics; using Microsoft.Extensions.Options; using Moq; @@ -25,7 +26,8 @@ public void Ctor_ThrowsExceptionsWhenNullArgs() null, new NullLoggerFactory().CreateLogger<RateLimitingMiddleware>(), options, - Mock.Of<IServiceProvider>())); + Mock.Of<IServiceProvider>(), + new RateLimitingMetrics(new TestMeterFactory()))); Assert.Throws<ArgumentNullException>(() => new RateLimitingMiddleware(c => { @@ -33,7 +35,8 @@ public void Ctor_ThrowsExceptionsWhenNullArgs() }, null, options, - Mock.Of<IServiceProvider>())); + Mock.Of<IServiceProvider>(), + new RateLimitingMetrics(new TestMeterFactory()))); Assert.Throws<ArgumentNullException>(() => new RateLimitingMiddleware(c => { @@ -41,6 +44,16 @@ public void Ctor_ThrowsExceptionsWhenNullArgs() }, new NullLoggerFactory().CreateLogger<RateLimitingMiddleware>(), options, + null, + new RateLimitingMetrics(new TestMeterFactory()))); + + Assert.Throws<ArgumentNullException>(() => new RateLimitingMiddleware(c => + { + return Task.CompletedTask; + }, + new NullLoggerFactory().CreateLogger<RateLimitingMiddleware>(), + options, + Mock.Of<IServiceProvider>(), null)); } @@ -59,7 +72,8 @@ public async Task RequestsCallNextIfAccepted() }, new NullLoggerFactory().CreateLogger<RateLimitingMiddleware>(), options, - Mock.Of<IServiceProvider>()); + Mock.Of<IServiceProvider>(), + new RateLimitingMetrics(new TestMeterFactory())); // Act await middleware.Invoke(new DefaultHttpContext()); @@ -637,7 +651,8 @@ private RateLimitingMiddleware CreateTestRateLimitingMiddleware(IOptions<RateLim }, logger ?? new NullLoggerFactory().CreateLogger<RateLimitingMiddleware>(), options, - serviceProvider ?? Mock.Of<IServiceProvider>()); + serviceProvider ?? Mock.Of<IServiceProvider>(), + new RateLimitingMetrics(new TestMeterFactory())); private IOptions<RateLimiterOptions> CreateOptionsAccessor() => Options.Create(new RateLimiterOptions()); } diff --git a/src/Middleware/RateLimiting/test/TestPartitionedRateLimiter.cs b/src/Middleware/RateLimiting/test/TestPartitionedRateLimiter.cs index fa19f79779b7..f84dc427e692 100644 --- a/src/Middleware/RateLimiting/test/TestPartitionedRateLimiter.cs +++ b/src/Middleware/RateLimiting/test/TestPartitionedRateLimiter.cs @@ -12,18 +12,18 @@ namespace Microsoft.AspNetCore.RateLimiting; internal class TestPartitionedRateLimiter<TResource> : PartitionedRateLimiter<TResource> { - private List<RateLimiter> limiters = new List<RateLimiter>(); + private readonly List<RateLimiter> _limiters = new List<RateLimiter>(); public TestPartitionedRateLimiter() { } public TestPartitionedRateLimiter(RateLimiter limiter) { - limiters.Add(limiter); + _limiters.Add(limiter); } public void AddLimiter(RateLimiter limiter) { - limiters.Add(limiter); + _limiters.Add(limiter); } public override RateLimiterStatistics GetStatistics(TResource resourceID) @@ -36,9 +36,9 @@ protected override RateLimitLease AttemptAcquireCore(TResource resourceID, int p if (permitCount != 1) { throw new ArgumentException("Tests only support 1 permit at a time"); - } + } var leases = new List<RateLimitLease>(); - foreach (var limiter in limiters) + foreach (var limiter in _limiters) { var lease = limiter.AttemptAcquire(); if (lease.IsAcquired) @@ -64,7 +64,7 @@ protected override async ValueTask<RateLimitLease> AcquireAsyncCore(TResource re throw new ArgumentException("Tests only support 1 permit at a time"); } var leases = new List<RateLimitLease>(); - foreach (var limiter in limiters) + foreach (var limiter in _limiters) { leases.Add(await limiter.AcquireAsync()); } @@ -77,9 +77,8 @@ protected override async ValueTask<RateLimitLease> AcquireAsyncCore(TResource re unusedLease.Dispose(); } return new TestRateLimitLease(false, null); - } + } } return new TestRateLimitLease(true, leases); - } } diff --git a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj index f41aa21dc843..e4b328c02a61 100644 --- a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj +++ b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj @@ -20,8 +20,6 @@ <Compile Include="$(KestrelSharedSourceRoot)\HPackHeaderWriter.cs" Link="Internal\Http2\HPackHeaderWriter.cs" /> <Compile Include="$(KestrelSharedSourceRoot)\Http2HeadersEnumerator.cs" Link="Internal\Http2\Http2HeadersEnumerator.cs" /> <Compile Include="$(SharedSourceRoot)CertificateGeneration\**\*.cs" /> - <Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" /> - <Compile Include="$(SharedSourceRoot)UrlDecoder\**\*.cs" /> <Compile Include="$(SharedSourceRoot)InternalHeaderNames.cs" Link="Shared\InternalHeaderNames.cs"/> <Compile Include="$(SharedSourceRoot)Buffers\**\*.cs" LinkBase="Internal\Infrastructure\PipeWriterHelpers" /> <Compile Include="$(SharedSourceRoot)runtime\*.cs" Link="Shared\runtime\%(Filename)%(Extension)" /> diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http2/Http2RequestTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http2/Http2RequestTests.cs index 0109d41a2d42..0edf0ee3ea5d 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http2/Http2RequestTests.cs +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http2/Http2RequestTests.cs @@ -115,6 +115,7 @@ public async Task GET_RequestReturnsLargeData_GracefulShutdownDuringRequest_Requ private static async Task<(byte[], HttpResponseHeaders)> StartLongRunningRequestAsync(ILogger logger, IHost host, HttpMessageInvoker client) { var request = new HttpRequestMessage(HttpMethod.Get, $"http://127.0.0.1:{host.GetPort()}/"); + request.Headers.Host = "localhost2"; request.Version = HttpVersion.Version20; request.VersionPolicy = HttpVersionPolicy.RequestVersionExact;