File: ActivityGenerationWorker.cs
Web Access
Project: src\playground\TestShop\ActivityGenerator\ActivityGenerator.csproj (ActivityGenerator)
using System.Diagnostics;
 
namespace ActivityGenerator;
 
internal sealed class ActivityGenerationWorker(IHttpClientFactory httpClientFactory, ILogger<ActivityGenerationWorker> logger) : BackgroundService
{
    internal const string ActivitySourceName = "TestShop.ActivityGenerator";
    private const int WorkerCount = 4;
    private static readonly ActivitySource s_activitySource = new(ActivitySourceName);
    private static readonly (string Resource, string Path)[] s_routes =
    [
        ("frontend", "/"),
        ("frontend", "/?after=8"),
        ("frontend", "/?before=9"),
        ("frontend", "/catalog/images/1"),
        ("frontend", "/catalog/images/2"),
        ("frontend", "/catalog/images/3"),
        ("apigateway", "/catalog/api/v1/catalog/items/type/all/brand?pageSize=8"),
        ("apigateway", "/catalog/api/v1/catalog/items/type/all/brand?after=8&pageSize=8"),
        ("catalogservice", "/api/v1/catalog/items/type/all/brand?pageSize=8")
    ];
 
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        logger.LogInformation("Generating read-only TestShop activity with {WorkerCount} workers until stopped.", WorkerCount);
 
        try
        {
            await Task.WhenAll(Enumerable.Range(0, WorkerCount).Select(worker => GenerateActivityAsync(worker, stoppingToken)));
        }
        catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
        {
            // Stopping the resource cancels both in-flight requests and worker delays.
        }
 
        logger.LogInformation("Activity generation stopped.");
    }
 
    private async Task GenerateActivityAsync(int worker, CancellationToken stoppingToken)
    {
        var routeIndex = worker;
 
        while (!stoppingToken.IsCancellationRequested)
        {
            var route = s_routes[routeIndex];
            await SendRequestAsync(route.Resource, route.Path, stoppingToken);
            routeIndex = (routeIndex + 1) % s_routes.Length;
 
            await Task.Delay(TimeSpan.FromMilliseconds(500), stoppingToken);
        }
    }
 
    private async Task SendRequestAsync(string resource, string path, CancellationToken stoppingToken)
    {
        using var activity = s_activitySource.StartActivity("Generate activity");
        activity?.SetTag("loadtest.resource", resource);
        activity?.SetTag("loadtest.path", path);
        var start = Stopwatch.GetTimestamp();
 
        try
        {
            using var client = httpClientFactory.CreateClient(resource);
            using var response = await client.GetAsync(path, stoppingToken);
 
            if (response.IsSuccessStatusCode)
            {
                logger.LogInformation("GET {Resource}{Path} returned {StatusCode} in {ElapsedMilliseconds} ms.",
                    resource, path, (int)response.StatusCode, Stopwatch.GetElapsedTime(start).TotalMilliseconds);
            }
            else
            {
                activity?.SetStatus(ActivityStatusCode.Error, response.ReasonPhrase);
                logger.LogWarning("GET {Resource}{Path} returned {StatusCode}.", resource, path, (int)response.StatusCode);
            }
        }
        catch (HttpRequestException ex)
        {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            logger.LogWarning(ex, "GET {Resource}{Path} failed.", resource, path);
        }
        catch (OperationCanceledException) when (!stoppingToken.IsCancellationRequested)
        {
            activity?.SetStatus(ActivityStatusCode.Error, "Request timed out.");
            logger.LogWarning("GET {Resource}{Path} timed out.", resource, path);
        }
    }
}