From ac607d1ea110df8bd20b861f75e38968e42d1b8c Mon Sep 17 00:00:00 2001 From: Tiago Araujo Date: Wed, 8 May 2024 12:46:00 +0100 Subject: [PATCH] feat: inc and dec gauge metrics --- Makefile | 3 + .../Resolvers/HttpHeaderPriorityResolver.cs | 19 +--- .../IAdaptativeLimiterOptionsExtensions.cs | 32 ++++--- .../HttpRequestsConcurrencyItemsGauge.cs | 22 +++-- .../Metrics/HttpRequestsQueueItemsGauge.cs | 20 +++-- .../Tasks/ConcurrentCounter.cs | 5 +- .../Tasks/PriorityExtensions.cs | 45 ++++++++++ src/Farfetch.LoadShedding/Tasks/TaskQueue.cs | 38 ++++---- tests/Directory.Build.props | 3 +- ...arfetch.LoadShedding.BenchmarkTests.csproj | 4 - .../TaskQueueBenchmarks.cs | 12 +-- tests/integration-tests/Directory.Build.props | 7 ++ .../Base/Controllers/WebApiTestController.cs | 13 ++- .../AdaptativeConcurrencyLimiterTests.cs | 87 ++++++++++++++----- ...fetch.LoadShedding.PerformanceTests.csproj | 4 - tests/unit-tests/Directory.Build.props | 8 ++ .../Tasks/PriorityExtensionsTests.cs | 52 +++++++++++ .../Tasks/TaskItemTests.cs | 8 +- .../Tasks/TaskManagerTests.cs | 19 ++-- .../Tasks/TaskQueueTests.cs | 8 +- 20 files changed, 280 insertions(+), 129 deletions(-) create mode 100644 src/Farfetch.LoadShedding/Tasks/PriorityExtensions.cs create mode 100644 tests/integration-tests/Directory.Build.props create mode 100644 tests/unit-tests/Directory.Build.props create mode 100644 tests/unit-tests/Farfetch.LoadShedding.Tests/Tasks/PriorityExtensionsTests.cs diff --git a/Makefile b/Makefile index 3afb631..dc958e5 100644 --- a/Makefile +++ b/Makefile @@ -5,3 +5,6 @@ sample: sample-clean: @docker compose -f docker-compose.sample.yaml down + +benchmark: + dotnet run -c Release --project ./tests/benchmark/Farfetch.LoadShedding.BenchmarkTests/Farfetch.LoadShedding.BenchmarkTests.csproj diff --git a/src/Farfetch.LoadShedding.AspNetCore/Resolvers/HttpHeaderPriorityResolver.cs b/src/Farfetch.LoadShedding.AspNetCore/Resolvers/HttpHeaderPriorityResolver.cs index 0a56ded..10d5cae 100644 --- a/src/Farfetch.LoadShedding.AspNetCore/Resolvers/HttpHeaderPriorityResolver.cs +++ b/src/Farfetch.LoadShedding.AspNetCore/Resolvers/HttpHeaderPriorityResolver.cs @@ -1,4 +1,3 @@ -using System; using System.Threading.Tasks; using Farfetch.LoadShedding.Tasks; using Microsoft.AspNetCore.Http; @@ -9,8 +8,6 @@ internal class HttpHeaderPriorityResolver : IPriorityResolver { internal const string DefaultPriorityHeaderName = "X-Priority"; - private const string Separator = "-"; - private readonly string _headerName; public HttpHeaderPriorityResolver() @@ -30,21 +27,7 @@ public Task ResolveAsync(HttpContext context) return Task.FromResult(Priority.Normal); } - var normalizedValue = this.NormalizeHeaderValue(values); - - if (!Enum.TryParse(normalizedValue, true, out Priority priority)) - { - priority = Priority.Normal; - } - - return Task.FromResult(priority); - } - - private string NormalizeHeaderValue(string headerValue) - { - return headerValue - .Replace(Separator, string.Empty) - .ToLower(); + return Task.FromResult(values.ToString().ParsePriority()); } } } diff --git a/src/Farfetch.LoadShedding.Prometheus/IAdaptativeLimiterOptionsExtensions.cs b/src/Farfetch.LoadShedding.Prometheus/IAdaptativeLimiterOptionsExtensions.cs index a657132..94ecfb7 100644 --- a/src/Farfetch.LoadShedding.Prometheus/IAdaptativeLimiterOptionsExtensions.cs +++ b/src/Farfetch.LoadShedding.Prometheus/IAdaptativeLimiterOptionsExtensions.cs @@ -66,9 +66,10 @@ private static void SubscribeItemDequeuedEvent( events.ItemDequeued.Subscribe(args => { var method = accessor.GetMethod(); + var priority = args.Priority.FormatPriority(); - queueItemsGauge.Set(method, args.Priority.FormatPriority(), args.QueueCount); - queueTimeHistogram.Observe(method, args.Priority.FormatPriority(), args.QueueTime.TotalSeconds); + queueItemsGauge.Decrement(method, priority); + queueTimeHistogram.Observe(method, priority, args.QueueTime.TotalSeconds); }); } @@ -88,7 +89,7 @@ private static void SubscribeItemProcessedEvent( var method = accessor.GetMethod(); var priority = args.Priority.FormatPriority(); - concurrencyItemsGauge.Set(method, priority, args.ConcurrencyCount); + concurrencyItemsGauge.Decrement(method, priority); taskProcessingTimeHistogram.Observe(method, priority, args.ProcessingTime.TotalSeconds); }); } @@ -123,10 +124,10 @@ private static void SubscribeItemProcessingEvent( events.ItemProcessing.Subscribe(args => { - concurrencyItemsGauge.Set( - accessor.GetMethod(), - args.Priority.FormatPriority(), - args.ConcurrencyCount); + var method = accessor.GetMethod(); + var priority = args.Priority.FormatPriority(); + + concurrencyItemsGauge.Increment(method, priority); }); } @@ -139,10 +140,10 @@ private static void SubscribeItemEnqueuedEvent(this Events.ILoadSheddingEvents e events.ItemEnqueued.Subscribe(args => { - queueItemsGauge.Set( - accessor.GetMethod(), - args.Priority.FormatPriority(), - args.QueueCount); + var method = accessor.GetMethod(); + var priority = args.Priority.FormatPriority(); + + queueItemsGauge.Increment(method, priority); }); } @@ -155,8 +156,10 @@ private static void SubscribeRejectedEvent(this Events.ILoadSheddingEvents event events.Rejected.Subscribe(args => { + var method = accessor.GetMethod(); + rejectedCounter.Increment( - accessor.GetMethod(), + method, args.Priority.FormatPriority(), args.Reason); }); @@ -171,10 +174,5 @@ private static string GetMethod(this IHttpContextAccessor accessor) return accessor.HttpContext.Request.Method.ToUpper(); } - - private static string FormatPriority(this Priority priority) - { - return priority.ToString().ToLower(); - } } } diff --git a/src/Farfetch.LoadShedding.Prometheus/Metrics/HttpRequestsConcurrencyItemsGauge.cs b/src/Farfetch.LoadShedding.Prometheus/Metrics/HttpRequestsConcurrencyItemsGauge.cs index 223d725..932ebe0 100644 --- a/src/Farfetch.LoadShedding.Prometheus/Metrics/HttpRequestsConcurrencyItemsGauge.cs +++ b/src/Farfetch.LoadShedding.Prometheus/Metrics/HttpRequestsConcurrencyItemsGauge.cs @@ -1,5 +1,4 @@ using Prometheus; - using PrometheusBase = Prometheus; namespace Farfetch.LoadShedding.Prometheus.Metrics @@ -22,16 +21,27 @@ internal HttpRequestsConcurrencyItemsGauge( protected override string DefaultName => "http_requests_concurrency_items_total"; /// - /// Sets the value of the gauge. + /// Increments the value of the gauge. + /// + /// The method. + /// The priority. + public void Increment(string method, string priority) + { + this.Metric? + .WithLabels(method, priority) + .Inc(); + } + + /// + /// Decrements the value of the gauge. /// /// The method. /// The priority. - /// The value. - public void Set(string method, string priority, double value) + public void Decrement(string method, string priority) { this.Metric? .WithLabels(method, priority) - .Set(value); + .Dec(); } /// @@ -40,7 +50,7 @@ protected override Gauge Create(CollectorRegistry registry, MetricOptions option return PrometheusBase .Metrics .WithCustomRegistry(registry) - .CreateGauge(options.Name, Description, new PrometheusBase.GaugeConfiguration + .CreateGauge(options.Name, Description, new GaugeConfiguration { LabelNames = new[] { MetricsConstants.MethodLabel, MetricsConstants.PriorityLabel }, }); diff --git a/src/Farfetch.LoadShedding.Prometheus/Metrics/HttpRequestsQueueItemsGauge.cs b/src/Farfetch.LoadShedding.Prometheus/Metrics/HttpRequestsQueueItemsGauge.cs index bddbf51..81137a1 100644 --- a/src/Farfetch.LoadShedding.Prometheus/Metrics/HttpRequestsQueueItemsGauge.cs +++ b/src/Farfetch.LoadShedding.Prometheus/Metrics/HttpRequestsQueueItemsGauge.cs @@ -1,5 +1,4 @@ using Prometheus; - using PrometheusBase = Prometheus; namespace Farfetch.LoadShedding.Prometheus.Metrics @@ -22,16 +21,27 @@ internal HttpRequestsQueueItemsGauge( protected override string DefaultName => "http_requests_queue_items_total"; /// - /// Sets the value of the gauge. + /// Increments the value of the gauge. + /// + /// The method. + /// The priority. + public void Increment(string method, string priority) + { + this.Metric? + .WithLabels(method, priority) + .Inc(); + } + + /// + /// Decrements the value of the gauge. /// /// The method. /// The priority. - /// The value. - public void Set(string method, string priority, double value) + public void Decrement(string method, string priority) { this.Metric? .WithLabels(method, priority) - .Set(value); + .Dec(); } /// diff --git a/src/Farfetch.LoadShedding/Tasks/ConcurrentCounter.cs b/src/Farfetch.LoadShedding/Tasks/ConcurrentCounter.cs index 1bc5308..722d482 100644 --- a/src/Farfetch.LoadShedding/Tasks/ConcurrentCounter.cs +++ b/src/Farfetch.LoadShedding/Tasks/ConcurrentCounter.cs @@ -64,10 +64,7 @@ public int Decrement() { lock (this._locker) { - if (this._count > 0) - { - this._count--; - } + this._count--; return this._count; } diff --git a/src/Farfetch.LoadShedding/Tasks/PriorityExtensions.cs b/src/Farfetch.LoadShedding/Tasks/PriorityExtensions.cs new file mode 100644 index 0000000..33ee21d --- /dev/null +++ b/src/Farfetch.LoadShedding/Tasks/PriorityExtensions.cs @@ -0,0 +1,45 @@ +using System; + +namespace Farfetch.LoadShedding.Tasks +{ + /// + /// Extension methods of + /// + public static class PriorityExtensions + { + /// + /// Parses a string value into a enum. + /// The parser is case-insensetive and accepts hyphen. e.g. Normal, Non-Critival or CRITICAL. + /// + /// Priority string value to be parsed. + /// The Priority parsed value. Returns Priority.Normal if the value is invalid. + public static Priority ParsePriority(this string value) + { + if (value is null) + { + return Priority.Normal; + } + + var normalizedValue = value + .Replace("-", string.Empty) + .ToLowerInvariant(); + + if (Enum.TryParse(normalizedValue, true, out Priority priority)) + { + return priority; + } + + return Priority.Normal; + } + + /// + /// Format a enum to a string. + /// + /// Priority value to be formatted. + /// The Priority format string. + public static string FormatPriority(this Priority priority) + { + return priority.ToString().ToLowerInvariant(); + } + } +} diff --git a/src/Farfetch.LoadShedding/Tasks/TaskQueue.cs b/src/Farfetch.LoadShedding/Tasks/TaskQueue.cs index 4d28890..73fb6d4 100644 --- a/src/Farfetch.LoadShedding/Tasks/TaskQueue.cs +++ b/src/Farfetch.LoadShedding/Tasks/TaskQueue.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; @@ -11,12 +9,7 @@ internal class TaskQueue { private readonly ConcurrentCounter _counter = new(); - private readonly IDictionary _queues = new SortedDictionary() - { - [Priority.Critical] = new TaskItemList(), - [Priority.Normal] = new TaskItemList(), - [Priority.NonCritical] = new TaskItemList(), - }; + private readonly TaskItemList[] _queues = new TaskItemList[3] { new(), new(), new() }; public TaskQueue(int limit) { @@ -44,9 +37,8 @@ public void Enqueue(TaskItem item) public TaskItem Dequeue() { var nextQueueItem = this._queues - .FirstOrDefault(x => x.Value.HasItems) - .Value? - .Dequeue(); + .FirstOrDefault(x => x.HasItems) + ?.Dequeue(); if (nextQueueItem != null) { @@ -58,7 +50,7 @@ public TaskItem Dequeue() public void Remove(TaskItem item) { - if (this._queues[item.Priority].Remove(item)) + if (this._queues[(int)item.Priority].Remove(item)) { this.DecrementCounter(); } @@ -68,25 +60,22 @@ internal void Clear() { foreach (var queue in this._queues) { - queue.Value.Clear(); + queue.Clear(); } } private int EnqueueItem(TaskItem item) { - this._queues[item.Priority].Add(item); - - var count = this._counter.Increment(); + this._queues[(int)item.Priority].Add(item); - return count; + return this.IncrementCounter(); } private void RejectLastItem() { var lastItem = this._queues - .LastOrDefault(x => x.Value.HasItems) - .Value? - .DequeueLast(); + .LastOrDefault(x => x.HasItems) + ?.DequeueLast(); if (lastItem == null) { @@ -98,9 +87,14 @@ private void RejectLastItem() lastItem.Reject(); } - private void DecrementCounter() + private int IncrementCounter() + { + return this._counter.Increment(); + } + + private int DecrementCounter() { - this._counter.Decrement(); + return this._counter.Decrement(); } } } diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 1b0c8ed..ea593c9 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -2,7 +2,6 @@ - - + diff --git a/tests/benchmark/Farfetch.LoadShedding.BenchmarkTests/Farfetch.LoadShedding.BenchmarkTests.csproj b/tests/benchmark/Farfetch.LoadShedding.BenchmarkTests/Farfetch.LoadShedding.BenchmarkTests.csproj index acd0f7f..9c86fc7 100644 --- a/tests/benchmark/Farfetch.LoadShedding.BenchmarkTests/Farfetch.LoadShedding.BenchmarkTests.csproj +++ b/tests/benchmark/Farfetch.LoadShedding.BenchmarkTests/Farfetch.LoadShedding.BenchmarkTests.csproj @@ -18,8 +18,4 @@ - - - - diff --git a/tests/benchmark/Farfetch.LoadShedding.BenchmarkTests/TaskQueueBenchmarks.cs b/tests/benchmark/Farfetch.LoadShedding.BenchmarkTests/TaskQueueBenchmarks.cs index 1d35106..4de6ccb 100644 --- a/tests/benchmark/Farfetch.LoadShedding.BenchmarkTests/TaskQueueBenchmarks.cs +++ b/tests/benchmark/Farfetch.LoadShedding.BenchmarkTests/TaskQueueBenchmarks.cs @@ -1,4 +1,4 @@ -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Order; using Farfetch.LoadShedding.Tasks; @@ -13,11 +13,11 @@ namespace Farfetch.LoadShedding.BenchmarkTests [RankColumn] public class TaskQueueBenchmarks { - private readonly TaskQueue _queue = new TaskQueue(int.MaxValue); + private readonly TaskQueue _queue = new(int.MaxValue); - private readonly TaskQueue _emptyQueue = new TaskQueue(int.MaxValue); + private readonly TaskQueue _emptyQueue = new(int.MaxValue); - private readonly TaskQueue _limitedQueue = new TaskQueue(1000); + private readonly TaskQueue _limitedQueue = new(1000); [IterationSetup] public void Initialize() @@ -38,7 +38,7 @@ public void Initialize() } [Benchmark] - public void TaskQueueWith1000Items_EnqueueFixedPriority() => this._queue.Enqueue(new TaskItem(0)); + public void TaskQueueWith1000Items_EnqueueFixedPriority() => this._queue.Enqueue(new TaskItem(Priority.Critical)); [Benchmark] public void TaskQueueEmpty_EnqueueRandomPriority() => this._emptyQueue.Enqueue(GetTaskRandomPriority()); @@ -50,7 +50,7 @@ public void Initialize() public void TaskQueueWith1000Items_Dequeue() => this._queue.Dequeue(); [Benchmark] - public void TaskQueue_EnqueueNewItem_LimitReached() => this._limitedQueue.Enqueue(new TaskItem(0)); + public void TaskQueue_EnqueueNewItem_LimitReached() => this._limitedQueue.Enqueue(new TaskItem(Priority.Critical)); private static TaskItem GetTaskRandomPriority() { diff --git a/tests/integration-tests/Directory.Build.props b/tests/integration-tests/Directory.Build.props new file mode 100644 index 0000000..d1dc7cd --- /dev/null +++ b/tests/integration-tests/Directory.Build.props @@ -0,0 +1,7 @@ + + + + + + + diff --git a/tests/integration-tests/Farfetch.LoadShedding.IntegrationTests/Base/Controllers/WebApiTestController.cs b/tests/integration-tests/Farfetch.LoadShedding.IntegrationTests/Base/Controllers/WebApiTestController.cs index a0b6b1d..0505f8d 100644 --- a/tests/integration-tests/Farfetch.LoadShedding.IntegrationTests/Base/Controllers/WebApiTestController.cs +++ b/tests/integration-tests/Farfetch.LoadShedding.IntegrationTests/Base/Controllers/WebApiTestController.cs @@ -1,6 +1,7 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Farfetch.LoadShedding.AspNetCore.Attributes; using Farfetch.LoadShedding.IntegrationTests.Base.Models; +using Farfetch.LoadShedding.Tasks; using Microsoft.AspNetCore.Mvc; namespace Farfetch.LoadShedding.IntegrationTests.Base.Controllers @@ -12,7 +13,7 @@ public class WebApiTestController : ControllerBase { [HttpGet] [Route("people")] - [EndpointPriority(Tasks.Priority.Critical)] + [EndpointPriority(Priority.Critical)] public async Task GetPeopleAsync() { await Task.Delay(500); @@ -27,5 +28,13 @@ public async Task GetPeopleAsync() }, }); } + + [HttpDelete] + [Route("people")] + [EndpointPriority(Priority.Critical)] + public Task DeletePeopleAsync() + { + return Task.Delay(500); + } } } diff --git a/tests/integration-tests/Farfetch.LoadShedding.IntegrationTests/Tests/Limiters/AdaptativeConcurrencyLimiterTests.cs b/tests/integration-tests/Farfetch.LoadShedding.IntegrationTests/Tests/Limiters/AdaptativeConcurrencyLimiterTests.cs index 8860792..c6508d4 100644 --- a/tests/integration-tests/Farfetch.LoadShedding.IntegrationTests/Tests/Limiters/AdaptativeConcurrencyLimiterTests.cs +++ b/tests/integration-tests/Farfetch.LoadShedding.IntegrationTests/Tests/Limiters/AdaptativeConcurrencyLimiterTests.cs @@ -10,25 +10,28 @@ using Microsoft.Extensions.DependencyInjection; using Prometheus; using Xunit; +using Xunit.Abstractions; namespace Farfetch.LoadShedding.IntegrationTests.Tests.Limiters { public class AdaptativeConcurrencyLimiterTests { - private readonly ConcurrentBag _queueLimits = new ConcurrentBag(); - private readonly ConcurrentBag _concurrencyLimits = new ConcurrentBag(); - private readonly ConcurrentBag _enqueuedItems = new ConcurrentBag(); + private readonly ConcurrentBag _queueLimits = new(); + private readonly ConcurrentBag _concurrencyLimits = new(); + private readonly ConcurrentBag _enqueuedItems = new(); private readonly CollectorRegistry _collectorRegistry; + private readonly ITestOutputHelper output; private int _numberOfRejectedRequests; /// /// Initializes a new instance of the class. /// - public AdaptativeConcurrencyLimiterTests() + public AdaptativeConcurrencyLimiterTests(ITestOutputHelper output) { this._numberOfRejectedRequests = 0; this._collectorRegistry = new CollectorRegistry(); + this.output = output; } /// @@ -36,7 +39,7 @@ public AdaptativeConcurrencyLimiterTests() /// /// A representing the result of the asynchronous operation. [Fact] - public async Task GetAsync_WithReducedLimitAndQueueSize_SomeRequestsAreRejected() + public async Task GetAsync_WithReducedLimitAndQueueSizeAndMultiplePrioritiesAndMethods_SomeRequestsAreRejected() { // Arrange const int InitialConcurrencyLimit = 100, InitialQueueSize = 4, MinSuccessfulRequests = 44; @@ -49,19 +52,24 @@ public async Task GetAsync_WithReducedLimitAndQueueSize_SomeRequestsAreRejected( MinQueueSize = InitialQueueSize, }; - var client = this.GetClient(options); + var client = this.GetClient(options, x => x.UseHeaderPriorityResolver()); // Act var tasks = Enumerable .Range(0, 1000) - .Select(_ => Task.Run(() => client.GetAsync("/api/people"))); + .Select(i => Task.Run(() => + { + var message = new HttpRequestMessage(i % 3 == 0 ? HttpMethod.Delete : HttpMethod.Get, "/api/people"); + message.Headers.Add("X-Priority", i % 2 == 0 ? "critical" : "normal"); + return client.SendAsync(message); + })); var results = await Task.WhenAll(tasks.ToArray()); // Assert Assert.True(results.Count(x => x.IsSuccessStatusCode) >= MinSuccessfulRequests); Assert.Contains(results, x => x.StatusCode == HttpStatusCode.ServiceUnavailable); - await AssertMetrics(client, Priority.Normal); + await AssertMetrics(client, labels: new[] { ("GET", "normal"), ("GET", "critical"), ("DELETE", "normal"), ("DELETE", "critical") }, hadQueueItems: true, hadRejectedItems: true); } [Fact] @@ -96,7 +104,7 @@ public async Task GetAsync_WithReducedLimitAndQueueSizeAndMultiplePriorities_Som Assert.True(results.Count(x => x.IsSuccessStatusCode) >= MinSuccessfulRequests); Assert.Contains(results, x => x.StatusCode == HttpStatusCode.ServiceUnavailable); - await AssertMetrics(client, Priority.Critical, Priority.Normal, Priority.NonCritical); + await AssertMetrics(client, labels: new[] { ("GET", "critical"), ("GET", "normal"), ("GET", "noncritical") }, hadQueueItems: true, hadRejectedItems: true); } /// @@ -128,6 +136,7 @@ public async Task GetAsync_WithHighLimitAndQueueSize_NoneRequestsIsRejected() // Assert Assert.Equal(ExpectedSuccessfulRequests, results.Count(x => x.IsSuccessStatusCode)); Assert.Equal(ExpectedRejectedRequests, results.Count(x => x.StatusCode == HttpStatusCode.ServiceUnavailable)); + await AssertMetrics(client, priority: "normal", hadQueueItems: false, hadRejectedItems: false); } [Theory] @@ -162,6 +171,7 @@ public async Task GetAsync_WithHeaderPriority_ResolvePriorityFromHeaderValue(str // Assert Assert.NotEmpty(_enqueuedItems); Assert.True(_enqueuedItems.All(x => x == priority)); + await AssertMetrics(client, priority.FormatPriority(), hadQueueItems: false, hadRejectedItems: false); } [Theory] @@ -196,6 +206,7 @@ public async Task GetAsync_WithEndpointPriority_ResolveFromEndpointPriorityAttri // Assert Assert.NotEmpty(_enqueuedItems); Assert.True(_enqueuedItems.All(x => x == Priority.Critical)); + await AssertMetrics(client, "critical", hadQueueItems: false, hadRejectedItems: false); } /// @@ -229,6 +240,7 @@ public async Task GetAsync_WithListener_TheEventValuesAreCorrect() Assert.Equal(_numberOfRejectedRequests, results.Count(x => x.StatusCode == HttpStatusCode.ServiceUnavailable)); Assert.Contains(this._concurrencyLimits, x => x == InitialConcurrencyLimit); Assert.Contains(this._queueLimits, x => x == InitialQueueSize); + await AssertMetrics(client, "normal", hadQueueItems: false, hadRejectedItems: false); } /// @@ -315,7 +327,12 @@ public HttpClient GetClient( return testServer.CreateClient(); } - private static async Task AssertMetrics(HttpClient client, params Priority[] priorities) + private Task AssertMetrics(HttpClient client, string priority, bool hadQueueItems, bool hadRejectedItems) + { + return AssertMetrics(client, new[] { ("GET", priority) }, hadQueueItems, hadRejectedItems); + } + + private async Task AssertMetrics(HttpClient client, (string method, string priority)[] labels, bool hadQueueItems, bool hadRejectedItems) { var metrics = await client.GetAsync("/monitoring/metrics"); @@ -325,25 +342,51 @@ private static async Task AssertMetrics(HttpClient client, params Priority[] pri Assert.Equal("text/plain", metrics.Content?.Headers?.ContentType?.MediaType); var content = metrics.Content?.ReadAsStringAsync().GetAwaiter().GetResult(); + output.WriteLine(content); + + // Assert Concurrency + Assert.Contains("TYPE http_requests_concurrency_items_total gauge", content); + Assert.Contains("TYPE http_requests_concurrency_limit_total gauge", content); + foreach (var (method, priority) in labels) + { + Assert.Contains($"http_requests_concurrency_items_total{{method=\"{method}\",priority=\"{priority}\"}} 0", content); + } - Assert.Contains("http_requests_concurrency_items_total", content); Assert.Contains("http_requests_concurrency_limit_total", content); - Assert.Contains("http_requests_task_processing_time_seconds", content); - Assert.Contains("http_requests_queue_limit_total", content); - foreach(var priority in priorities) + // Assert Processing + Assert.Contains("TYPE http_requests_task_processing_time_seconds histogram", content); + foreach (var (method, priority) in labels) { - var priorityText = priority.ToString().ToLower(); - Assert.Contains($"http_requests_queue_items_total{{method=\"GET\",priority=\"{priorityText}\"}}", content); - Assert.Contains($"http_requests_queue_time_seconds_sum{{method=\"GET\",priority=\"{priorityText}\"}}", content); - Assert.Contains($"http_requests_queue_time_seconds_count{{method=\"GET\",priority=\"{priorityText}\"}}", content); - Assert.Contains($"http_requests_queue_time_seconds_bucket{{method=\"GET\",priority=\"{priorityText}\",le=\"0.0005\"}}", content); + Assert.Contains($"http_requests_task_processing_time_seconds_sum{{method=\"{method}\",priority=\"{priority}\"}}", content); + Assert.Contains($"http_requests_task_processing_time_seconds_count{{method=\"{method}\",priority=\"{priority}\"}}", content); + Assert.Contains($"http_requests_task_processing_time_seconds_bucket{{method=\"{method}\",priority=\"{priority}\",le=\"+Inf\"}}", content); } - var lowPriority = priorities.Max(); + // Assert Queue + Assert.Contains("TYPE http_requests_queue_items_total gauge", content); + Assert.Contains("TYPE http_requests_queue_time_seconds histogram", content); + if (hadQueueItems) + { + Assert.Contains("http_requests_queue_limit_total", content); + foreach (var (method, priority) in labels) + { + Assert.Contains($"http_requests_queue_items_total{{method=\"{method}\",priority=\"{priority}\"}} 0", content); + Assert.Contains($"http_requests_queue_time_seconds_sum{{method=\"{method}\",priority=\"{priority}\"}}", content); + Assert.Contains($"http_requests_queue_time_seconds_count{{method=\"{method}\",priority=\"{priority}\"}}", content); + Assert.Contains($"http_requests_queue_time_seconds_bucket{{method=\"{method}\",priority=\"{priority}\",le=\"+Inf\"}}", content); + } + } - Assert.Contains($"http_requests_rejected_total{{method=\"GET\",priority=\"{lowPriority.ToString().ToLower()}\",reason=\"max_queue_items\"}}", content); - Assert.DoesNotContain($"http_requests_rejected_total{{method=\"UNKNOWN\",priority=\"{lowPriority.ToString().ToLower()}\",reason=\"max_queue_items\"}}", content); + // Assert Rejection + Assert.Contains("TYPE http_requests_rejected_total counter", content); + if (hadRejectedItems) + { + foreach (var (method, priority) in labels) + { + Assert.Contains($"http_requests_rejected_total{{method=\"{method}\",priority=\"{priority}\",reason=\"max_queue_items\"}}", content); + } + } } } } diff --git a/tests/performance-tests/Farfetch.LoadShedding.PerformanceTests/Farfetch.LoadShedding.PerformanceTests.csproj b/tests/performance-tests/Farfetch.LoadShedding.PerformanceTests/Farfetch.LoadShedding.PerformanceTests.csproj index ca254b5..6fb0410 100644 --- a/tests/performance-tests/Farfetch.LoadShedding.PerformanceTests/Farfetch.LoadShedding.PerformanceTests.csproj +++ b/tests/performance-tests/Farfetch.LoadShedding.PerformanceTests/Farfetch.LoadShedding.PerformanceTests.csproj @@ -17,8 +17,4 @@ - - - - diff --git a/tests/unit-tests/Directory.Build.props b/tests/unit-tests/Directory.Build.props new file mode 100644 index 0000000..81b2e1a --- /dev/null +++ b/tests/unit-tests/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/tests/unit-tests/Farfetch.LoadShedding.Tests/Tasks/PriorityExtensionsTests.cs b/tests/unit-tests/Farfetch.LoadShedding.Tests/Tasks/PriorityExtensionsTests.cs new file mode 100644 index 0000000..4b136fd --- /dev/null +++ b/tests/unit-tests/Farfetch.LoadShedding.Tests/Tasks/PriorityExtensionsTests.cs @@ -0,0 +1,52 @@ +using Farfetch.LoadShedding.Tasks; +using Xunit; + +namespace Farfetch.LoadShedding.Tests.Tasks +{ + public class PriorityExtensionsTests + { + [Theory] + [InlineData("Critical", Priority.Critical)] + [InlineData("critical", Priority.Critical)] + [InlineData("criti-cal", Priority.Critical)] + [InlineData("critical-", Priority.Critical)] + [InlineData("-critical", Priority.Critical)] + [InlineData("--critical", Priority.Critical)] + [InlineData("NORMAL", Priority.Normal)] + [InlineData("normal", Priority.Normal)] + [InlineData("nor-mal", Priority.Normal)] + [InlineData("nor--mal", Priority.Normal)] + [InlineData("normal-", Priority.Normal)] + [InlineData("-normal", Priority.Normal)] + [InlineData("noncritical", Priority.NonCritical)] + [InlineData("non-critical", Priority.NonCritical)] + [InlineData("NON-critical", Priority.NonCritical)] + [InlineData("noncritical-", Priority.NonCritical)] + [InlineData("noncritical--", Priority.NonCritical)] + [InlineData("-NONCRITICAL", Priority.NonCritical)] + [InlineData("", Priority.Normal)] + [InlineData(null, Priority.Normal)] + [InlineData("other", Priority.Normal)] + public void ParsePriority(string? value, Priority priority) + { + // Act + var result = PriorityExtensions.ParsePriority(value); + + // Assert + Assert.Equal(priority, result); + } + + [Theory] + [InlineData(Priority.Critical, "critical")] + [InlineData(Priority.Normal, "normal")] + [InlineData(Priority.NonCritical, "noncritical")] + public void FormatPriority(Priority priority, string value) + { + // Act + var result = PriorityExtensions.FormatPriority(priority); + + // Assert + Assert.Equal(value, result); + } + } +} diff --git a/tests/unit-tests/Farfetch.LoadShedding.Tests/Tasks/TaskItemTests.cs b/tests/unit-tests/Farfetch.LoadShedding.Tests/Tasks/TaskItemTests.cs index ed50022..5ddfd6a 100644 --- a/tests/unit-tests/Farfetch.LoadShedding.Tests/Tasks/TaskItemTests.cs +++ b/tests/unit-tests/Farfetch.LoadShedding.Tests/Tasks/TaskItemTests.cs @@ -9,7 +9,7 @@ public class TaskItemTests public async Task WaitAsync_Process_ReturnsProcessingStatus() { // Arrange - var taskItem = new TaskItem(0); + var taskItem = new TaskItem(Priority.Critical); var waitingTask = taskItem.WaitAsync(10000, CancellationToken.None); @@ -26,7 +26,7 @@ public async Task WaitAsync_Process_ReturnsProcessingStatus() public async Task WaitAsync_TimeoutReached_ReturnsCanceledStatus() { // Arrange - var taskItem = new TaskItem(0); + var taskItem = new TaskItem(Priority.Critical); var waitingTask = taskItem.WaitAsync(1, CancellationToken.None); @@ -43,7 +43,7 @@ public async Task WaitAsync_TimeoutReached_ReturnsCanceledStatus() public async Task WaitAsync_CancelledToken_ReturnsCanceledStatus() { // Arrange - var taskItem = new TaskItem(0); + var taskItem = new TaskItem(Priority.Critical); using var source = new CancellationTokenSource(); @@ -62,7 +62,7 @@ public async Task WaitAsync_CancelledToken_ReturnsCanceledStatus() public async Task WaitAsync_Reject_ReturnsProcessingStatus() { // Arrange - var taskItem = new TaskItem(0); + var taskItem = new TaskItem(Priority.Critical); var waitingTask = taskItem.WaitAsync(10000, CancellationToken.None); diff --git a/tests/unit-tests/Farfetch.LoadShedding.Tests/Tasks/TaskManagerTests.cs b/tests/unit-tests/Farfetch.LoadShedding.Tests/Tasks/TaskManagerTests.cs index 0c9104d..8cfae47 100644 --- a/tests/unit-tests/Farfetch.LoadShedding.Tests/Tasks/TaskManagerTests.cs +++ b/tests/unit-tests/Farfetch.LoadShedding.Tests/Tasks/TaskManagerTests.cs @@ -33,11 +33,12 @@ public void TaskManager_ChangeValues_TheValuesShouldBeCorrectlyDefined() // Arrange const int ExpectedTotalCount = 20, ExpectedQueueSize = 20; - var target = new TaskManager(10, 10); - // Act - target.ConcurrencyLimit = ExpectedTotalCount; - target.QueueLimit = ExpectedQueueSize; + var target = new TaskManager(10, 10) + { + ConcurrencyLimit = ExpectedTotalCount, + QueueLimit = ExpectedQueueSize, + }; // Assert Assert.Equal(ExpectedTotalCount, target.ConcurrencyLimit); @@ -71,10 +72,11 @@ public void TaskManager_WhenMaxCountIsChanged_NotifyConcurrencyLimitChangedIsCal events.ConcurrencyLimitChanged.Subscribe(args => currentLimit = args.Limit); - var target = new TaskManager(10, 10, Timeout.Infinite, events); - // Act - target.ConcurrencyLimit = expectedMaxCount; + var target = new TaskManager(10, 10, Timeout.Infinite, events) + { + ConcurrencyLimit = expectedMaxCount, + }; // Assert Assert.Equal(expectedMaxCount, currentLimit); @@ -243,13 +245,12 @@ public async Task Release_ConcurrencyItemsIsNotZero_NotifyConcurrentItemsCountCh var processingItems = new List(); var events = new LoadSheddingEvents(); - events.ItemProcessed.Subscribe(args => processedItems.Add(args)); events.ItemProcessing.Subscribe(args => processingItems.Add(args)); var target = new TaskManager(10, 10, Timeout.Infinite, events); - var item = await target.AcquireAsync(0); + var item = await target.AcquireAsync(Priority.Critical); // Act item.Complete(); diff --git a/tests/unit-tests/Farfetch.LoadShedding.Tests/Tasks/TaskQueueTests.cs b/tests/unit-tests/Farfetch.LoadShedding.Tests/Tasks/TaskQueueTests.cs index cb0747b..11e86d1 100644 --- a/tests/unit-tests/Farfetch.LoadShedding.Tests/Tasks/TaskQueueTests.cs +++ b/tests/unit-tests/Farfetch.LoadShedding.Tests/Tasks/TaskQueueTests.cs @@ -19,7 +19,7 @@ public TaskQueueTests() public void Enqueue_QueueLimitNotReached_AddToQueue() { // Arrange - var task = new TaskItem(0); + var task = new TaskItem(Priority.Critical); // Act this._target.Enqueue(task); @@ -35,8 +35,8 @@ public void Enqueue_QueueLimitIsReached_RejectItem() // Arrange this._target.Limit = 1; - var firstTask = new TaskItem(0); - var lastTask = new TaskItem(0); + var firstTask = new TaskItem(Priority.Critical); + var lastTask = new TaskItem(Priority.Critical); this._target.Enqueue(firstTask); @@ -57,7 +57,7 @@ public void Enqueue_TaskWithHigherPriorityQueueLimitIsReached_EnqueueItemAndReje var lowPriorityTask = new TaskItem(Priority.NonCritical); this._target.Enqueue(lowPriorityTask); - var highPriorityTask = new TaskItem(0); + var highPriorityTask = new TaskItem(Priority.Critical); // Act this._target.Enqueue(highPriorityTask);