From 78011114afdd2dd716cd645746a7a03dda6f0bcb Mon Sep 17 00:00:00 2001 From: Matvey6M6 Date: Sun, 10 May 2026 22:12:33 +0400 Subject: [PATCH 1/2] Done Lab3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit + FileService файловый сервис, который читает события и сохраняет JSON сотрудников в S3. + LocalStack в AppHost. + Публикация событий из ServiceApi в SNS + Подписка SNS -> SQS, которую опрашивает FileService. + Endpoint GET /files/{id} для проверки сохранённого файла + Backend.IntegrationTests с интеграционными тестами. --- ...430#U0442#U043e#U0440#U043d#U043e#U0439.md | 33 +++ ApiGateway/ApiGateway.csproj | 25 ++ .../WeightedRoundRobinOptions.cs | 16 ++ .../WeightedRoundRobinBalancer.cs | 72 ++++++ ApiGateway/Program.cs | 43 ++++ ApiGateway/Properties/launchSettings.json | 14 ++ ApiGateway/appsettings.Development.json | 9 + ApiGateway/appsettings.json | 10 + ApiGateway/ocelot.json | 41 ++++ AspireApp/AspireApp.AppHost/AppHost.cs | 51 ++++ .../AspireApp.AppHost.csproj | 25 ++ .../Properties/launchSettings.json | 32 +++ .../appsettings.Development.json | 8 + AspireApp/AspireApp.AppHost/appsettings.json | 8 + .../AspireApp.ServiceDefaults.csproj | 21 ++ .../AspireApp.ServiceDefaults/Extensions.cs | 101 ++++++++ .../Backend.IntegrationTests.csproj | 31 +++ .../EmployeeExportIntegrationTests.cs | 151 ++++++++++++ Client.Wasm/Components/DataCard.razor | 81 +++++-- Client.Wasm/Components/StudentCard.razor | 14 +- Client.Wasm/wwwroot/appsettings.json | 2 +- CloudDevelopment.sln | 36 +++ .../Background/FileExportHealthCheck.cs | 17 ++ .../FileExportInfrastructureState.cs | 12 + .../Background/SnsSqsFileExportWorker.cs | 228 ++++++++++++++++++ .../Configuration/AwsStorageOptions.cs | 44 ++++ FileService/File.Service.csproj | 21 ++ FileService/Program.cs | 51 ++++ FileService/Properties/launchSettings.json | 14 ++ FileService/Storage/IEmployeeFileStorage.cs | 17 ++ FileService/Storage/S3EmployeeFileStorage.cs | 91 +++++++ FileService/appsettings.Development.json | 17 ++ FileService/appsettings.json | 18 ++ README.md | 182 +++++--------- .../Configuration/AwsMessagingOptions.cs | 34 +++ ServiceApi/Entities/Employee.cs | 39 +++ ServiceApi/Generator/EmployeeGenerator.cs | 92 +++++++ .../Generator/EmployeeGeneratorService.cs | 88 +++++++ .../Generator/IEmployeeGeneratorService.cs | 11 + .../Messaging/EmployeeGeneratedMessage.cs | 29 +++ .../Messaging/IEmployeeEventPublisher.cs | 14 ++ .../Messaging/SnsEmployeeEventPublisher.cs | 110 +++++++++ ServiceApi/Program.cs | 84 +++++++ ServiceApi/Properties/launchSettings.json | 14 ++ ServiceApi/Service.Api.csproj | 21 ++ ServiceApi/appsettings.Development.json | 19 ++ ServiceApi/appsettings.json | 17 ++ image.png | Bin 0 -> 49992 bytes image1.png | Bin 0 -> 21614 bytes image2.png | Bin 0 -> 40056 bytes 50 files changed, 1961 insertions(+), 147 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/#U0432#U043e#U043f#U0440#U043e#U0441-#U043f#U043e-#U043b#U0430#U0431#U043e#U0440#U0430#U0442#U043e#U0440#U043d#U043e#U0439.md create mode 100644 ApiGateway/ApiGateway.csproj create mode 100644 ApiGateway/Configuration/WeightedRoundRobinOptions.cs create mode 100644 ApiGateway/LoadBalancing/WeightedRoundRobinBalancer.cs create mode 100644 ApiGateway/Program.cs create mode 100644 ApiGateway/Properties/launchSettings.json create mode 100644 ApiGateway/appsettings.Development.json create mode 100644 ApiGateway/appsettings.json create mode 100644 ApiGateway/ocelot.json create mode 100644 AspireApp/AspireApp.AppHost/AppHost.cs create mode 100644 AspireApp/AspireApp.AppHost/AspireApp.AppHost.csproj create mode 100644 AspireApp/AspireApp.AppHost/Properties/launchSettings.json create mode 100644 AspireApp/AspireApp.AppHost/appsettings.Development.json create mode 100644 AspireApp/AspireApp.AppHost/appsettings.json create mode 100644 AspireApp/AspireApp.ServiceDefaults/AspireApp.ServiceDefaults.csproj create mode 100644 AspireApp/AspireApp.ServiceDefaults/Extensions.cs create mode 100644 Backend.IntegrationTests/Backend.IntegrationTests.csproj create mode 100644 Backend.IntegrationTests/EmployeeExportIntegrationTests.cs create mode 100644 FileService/Background/FileExportHealthCheck.cs create mode 100644 FileService/Background/FileExportInfrastructureState.cs create mode 100644 FileService/Background/SnsSqsFileExportWorker.cs create mode 100644 FileService/Configuration/AwsStorageOptions.cs create mode 100644 FileService/File.Service.csproj create mode 100644 FileService/Program.cs create mode 100644 FileService/Properties/launchSettings.json create mode 100644 FileService/Storage/IEmployeeFileStorage.cs create mode 100644 FileService/Storage/S3EmployeeFileStorage.cs create mode 100644 FileService/appsettings.Development.json create mode 100644 FileService/appsettings.json create mode 100644 ServiceApi/Configuration/AwsMessagingOptions.cs create mode 100644 ServiceApi/Entities/Employee.cs create mode 100644 ServiceApi/Generator/EmployeeGenerator.cs create mode 100644 ServiceApi/Generator/EmployeeGeneratorService.cs create mode 100644 ServiceApi/Generator/IEmployeeGeneratorService.cs create mode 100644 ServiceApi/Messaging/EmployeeGeneratedMessage.cs create mode 100644 ServiceApi/Messaging/IEmployeeEventPublisher.cs create mode 100644 ServiceApi/Messaging/SnsEmployeeEventPublisher.cs create mode 100644 ServiceApi/Program.cs create mode 100644 ServiceApi/Properties/launchSettings.json create mode 100644 ServiceApi/Service.Api.csproj create mode 100644 ServiceApi/appsettings.Development.json create mode 100644 ServiceApi/appsettings.json create mode 100644 image.png create mode 100644 image1.png create mode 100644 image2.png diff --git a/.github/ISSUE_TEMPLATE/#U0432#U043e#U043f#U0440#U043e#U0441-#U043f#U043e-#U043b#U0430#U0431#U043e#U0440#U0430#U0442#U043e#U0440#U043d#U043e#U0439.md b/.github/ISSUE_TEMPLATE/#U0432#U043e#U043f#U0440#U043e#U0441-#U043f#U043e-#U043b#U0430#U0431#U043e#U0440#U0430#U0442#U043e#U0440#U043d#U043e#U0439.md new file mode 100644 index 00000000..02038ea5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/#U0432#U043e#U043f#U0440#U043e#U0441-#U043f#U043e-#U043b#U0430#U0431#U043e#U0440#U0430#U0442#U043e#U0440#U043d#U043e#U0439.md @@ -0,0 +1,33 @@ +--- +name: Вопрос по лабораторной +about: Этот шаблон предназначен для того, чтобы студенты могли задать вопрос по лабораторной +title: Вопрос по лабораторной +labels: '' +assignees: Gwymlas, alxmcs, danlla + +--- + +**Меня зовут:** +Укажите свои ФИО + +**Я из группы:** +Укажите номер группы + +**У меня вопрос по лабе:** +Укажите номер и название лабораторной, по которой появился вопрос. + +**Мой вопрос:** +Максимально подробно опишите, что вы хотите узнать/что у вас не получается/что у вас не работает. При необходимости, добавьте примеры кода. Примеры кода должны быть оформлены с использованием md разметки, чтобы их можно было удобно воспринимать: + +```cs +public class Program +{ + public static void Main(string[] args) + { + System.Console.WriteLine("Hello, World!"); + } +} +``` + +**Дополнительная информация** +Опишите тут все, что не попадает под перечисленные ранее категории (если в том есть необходимость). \ No newline at end of file diff --git a/ApiGateway/ApiGateway.csproj b/ApiGateway/ApiGateway.csproj new file mode 100644 index 00000000..c8100831 --- /dev/null +++ b/ApiGateway/ApiGateway.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + ApiGateway + ApiGateway + + + + + + + + + + + + + Always + + + + diff --git a/ApiGateway/Configuration/WeightedRoundRobinOptions.cs b/ApiGateway/Configuration/WeightedRoundRobinOptions.cs new file mode 100644 index 00000000..d5547d71 --- /dev/null +++ b/ApiGateway/Configuration/WeightedRoundRobinOptions.cs @@ -0,0 +1,16 @@ +namespace ApiGateway.Configuration; + +public sealed class WeightedRoundRobinOptions +{ + public const string SectionName = "WeightedRoundRobin"; + + public List Nodes { get; init; } = new(); +} + +public sealed class ReplicaNodeOptions +{ + public string Host { get; init; } = string.Empty; + public int Port { get; init; } + public int Weight { get; init; } = 1; + public string ReplicaId { get; init; } = string.Empty; +} diff --git a/ApiGateway/LoadBalancing/WeightedRoundRobinBalancer.cs b/ApiGateway/LoadBalancing/WeightedRoundRobinBalancer.cs new file mode 100644 index 00000000..33425e09 --- /dev/null +++ b/ApiGateway/LoadBalancing/WeightedRoundRobinBalancer.cs @@ -0,0 +1,72 @@ +using ApiGateway.Configuration; +using Microsoft.Extensions.Options; +using Ocelot.LoadBalancer.Interfaces; +using Ocelot.Responses; +using Ocelot.Values; + +namespace ApiGateway.LoadBalancing; + +public sealed class WeightedRoundRobinBalancer : ILoadBalancer +{ + private static readonly object Sync = new(); + private readonly ILogger _logger; + private readonly List _rotation; + private int _currentIndex; + + public WeightedRoundRobinBalancer( + IOptions options, + ILogger logger) + { + _logger = logger; + _rotation = BuildRotation(options.Value.Nodes); + + if (_rotation.Count == 0) + { + throw new InvalidOperationException("Не настроены узлы для Weighted Round Robin балансировки."); + } + } + + public string Type => nameof(WeightedRoundRobinBalancer); + + public Task> LeaseAsync(HttpContext context) + { + lock (Sync) + { + if (_currentIndex >= _rotation.Count) + { + _currentIndex = 0; + } + + var next = _rotation[_currentIndex++]; + + _logger.LogInformation( + "Gateway routed request to {ReplicaAddress} by {BalancerType}", + next, + Type); + + return Task.FromResult>( + new OkResponse(next)); + } + } + + public void Release(ServiceHostAndPort hostAndPort) + { + } + + private static List BuildRotation(IEnumerable nodes) + { + var rotation = new List(); + + foreach (var node in nodes.Where(static n => !string.IsNullOrWhiteSpace(n.Host) && n.Port > 0)) + { + var normalizedWeight = Math.Max(1, node.Weight); + + for (var i = 0; i < normalizedWeight; i++) + { + rotation.Add(new ServiceHostAndPort(node.Host, node.Port)); + } + } + + return rotation; + } +} \ No newline at end of file diff --git a/ApiGateway/Program.cs b/ApiGateway/Program.cs new file mode 100644 index 00000000..dda7b95a --- /dev/null +++ b/ApiGateway/Program.cs @@ -0,0 +1,43 @@ +using ApiGateway.Configuration; +using ApiGateway.LoadBalancing; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); +builder.Services.AddServiceDiscovery(); +builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true); + +builder.Logging.ClearProviders(); +builder.Logging.AddJsonConsole(options => +{ + options.IncludeScopes = true; + options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ "; +}); + +builder.Services.Configure( + builder.Configuration.GetSection(WeightedRoundRobinOptions.SectionName)); + +builder.Services.AddOcelot(builder.Configuration) + .AddCustomLoadBalancer(sp => + new WeightedRoundRobinBalancer( + sp.GetRequiredService>(), + sp.GetRequiredService>())); + +builder.Services.AddCors(options => options.AddDefaultPolicy(policy => +{ + policy.WithOrigins(["http://localhost:5127", "https://localhost:7282"]); + policy.WithMethods("GET"); + policy.WithHeaders("Content-Type"); + policy.WithExposedHeaders("X-Service-Replica", "X-Service-Weight"); +})); + +var app = builder.Build(); + +app.UseCors(); +app.MapDefaultEndpoints(); + +await app.UseOcelot(); + +app.Run(); diff --git a/ApiGateway/Properties/launchSettings.json b/ApiGateway/Properties/launchSettings.json new file mode 100644 index 00000000..98385b13 --- /dev/null +++ b/ApiGateway/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:7200", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ApiGateway/appsettings.Development.json b/ApiGateway/appsettings.Development.json new file mode 100644 index 00000000..79cac825 --- /dev/null +++ b/ApiGateway/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Ocelot": "Information" + } + } +} diff --git a/ApiGateway/appsettings.json b/ApiGateway/appsettings.json new file mode 100644 index 00000000..cc426f6c --- /dev/null +++ b/ApiGateway/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Ocelot": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/ApiGateway/ocelot.json b/ApiGateway/ocelot.json new file mode 100644 index 00000000..7c8dceea --- /dev/null +++ b/ApiGateway/ocelot.json @@ -0,0 +1,41 @@ +{ + "Routes": [ + { + "UpstreamPathTemplate": "/employee", + "UpstreamHttpMethod": [ "GET" ], + "DownstreamPathTemplate": "/employee", + "DownstreamScheme": "https", + "LoadBalancerOptions": { + "Type": "WeightedRoundRobinBalancer" + }, + "DownstreamHostAndPorts": [ + { "Host": "localhost", "Port": 15000 }, + { "Host": "localhost", "Port": 15001 }, + { "Host": "localhost", "Port": 15002 }, + { "Host": "localhost", "Port": 15003 }, + { "Host": "localhost", "Port": 15004 } + ] + }, + { + "UpstreamPathTemplate": "/files/{id}", + "UpstreamHttpMethod": [ "GET" ], + "DownstreamPathTemplate": "/files/{id}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { "Host": "localhost", "Port": 16000 } + ] + } + ], + "GlobalConfiguration": { + "BaseUrl": "http://localhost:7200" + }, + "WeightedRoundRobin": { + "Nodes": [ + { "ReplicaId": "R1", "Host": "localhost", "Port": 15000, "Weight": 1 }, + { "ReplicaId": "R2", "Host": "localhost", "Port": 15001, "Weight": 2 }, + { "ReplicaId": "R3", "Host": "localhost", "Port": 15002, "Weight": 3 }, + { "ReplicaId": "R4", "Host": "localhost", "Port": 15003, "Weight": 2 }, + { "ReplicaId": "R5", "Host": "localhost", "Port": 15004, "Weight": 1 } + ] + } +} diff --git a/AspireApp/AspireApp.AppHost/AppHost.cs b/AspireApp/AspireApp.AppHost/AppHost.cs new file mode 100644 index 00000000..753b0e2d --- /dev/null +++ b/AspireApp/AspireApp.AppHost/AppHost.cs @@ -0,0 +1,51 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var cache = builder.AddRedis("employee-cache") + .WithRedisInsight(containerName: "employee-insight"); + +var localstack = builder.AddContainer("localstack", "localstack/localstack", "3.5") + .WithEnvironment("SERVICES", "s3,sns,sqs") + .WithEnvironment("AWS_DEFAULT_REGION", "us-east-1") + .WithEnvironment("DEBUG", "1") + .WithHttpEndpoint(port: 4566, targetPort: 4566, name: "http") + .WithExternalHttpEndpoints(); + +var gateway = builder.AddProject("api-gateway"); + +var replicaWeights = new[] { 1, 2, 3, 2, 1 }; + +var fileService = builder.AddProject("file-service", launchProfileName: null) + .WithHttpEndpoint(port: 16000) + .WithEnvironment("Aws__ServiceUrl", "http://localhost:4566") + .WithEnvironment("Aws__Region", "us-east-1") + .WithEnvironment("Aws__AccessKey", "test") + .WithEnvironment("Aws__SecretKey", "test") + .WithEnvironment("Aws__TopicName", "employee-generated-topic") + .WithEnvironment("Aws__QueueName", "employee-generated-queue") + .WithEnvironment("Aws__BucketName", "employee-files") + .WaitFor(localstack); + +for (var i = 0; i < 5; i++) +{ + var service = builder.AddProject($"service-api-{i}", launchProfileName: null) + .WithHttpsEndpoint(port: 15000 + i) + .WithReference(cache, "RedisCache") + .WithEnvironment("ReplicaId", "R" + (i + 1)) + .WithEnvironment("ReplicaWeight", replicaWeights[i].ToString()) + .WithEnvironment("Aws__ServiceUrl", "http://localhost:4566") + .WithEnvironment("Aws__Region", "us-east-1") + .WithEnvironment("Aws__AccessKey", "test") + .WithEnvironment("Aws__SecretKey", "test") + .WithEnvironment("Aws__TopicName", "employee-generated-topic") + .WaitFor(cache) + .WaitFor(localstack); + + gateway.WaitFor(service); +} + +gateway.WaitFor(fileService); + +builder.AddProject("employee") + .WaitFor(gateway); + +builder.Build().Run(); diff --git a/AspireApp/AspireApp.AppHost/AspireApp.AppHost.csproj b/AspireApp/AspireApp.AppHost/AspireApp.AppHost.csproj new file mode 100644 index 00000000..719a5b0c --- /dev/null +++ b/AspireApp/AspireApp.AppHost/AspireApp.AppHost.csproj @@ -0,0 +1,25 @@ + + + + + + Exe + net8.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/AspireApp/AspireApp.AppHost/Properties/launchSettings.json b/AspireApp/AspireApp.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000..df45cce3 --- /dev/null +++ b/AspireApp/AspireApp.AppHost/Properties/launchSettings.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17096;http://localhost:15155", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21139", + "DOTNET_DASHBOARD_OTLP_HTTP_ENDPOINT_URL": "https://localhost:21140", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22017" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15155", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19197", + "DOTNET_DASHBOARD_OTLP_HTTP_ENDPOINT_URL": "http://localhost:19198", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20116" + } + } + } +} diff --git a/AspireApp/AspireApp.AppHost/appsettings.Development.json b/AspireApp/AspireApp.AppHost/appsettings.Development.json new file mode 100644 index 00000000..167eb683 --- /dev/null +++ b/AspireApp/AspireApp.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Aspire.Hosting": "Information" + } + } +} diff --git a/AspireApp/AspireApp.AppHost/appsettings.json b/AspireApp/AspireApp.AppHost/appsettings.json new file mode 100644 index 00000000..167eb683 --- /dev/null +++ b/AspireApp/AspireApp.AppHost/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Aspire.Hosting": "Information" + } + } +} diff --git a/AspireApp/AspireApp.ServiceDefaults/AspireApp.ServiceDefaults.csproj b/AspireApp/AspireApp.ServiceDefaults/AspireApp.ServiceDefaults.csproj new file mode 100644 index 00000000..530518fa --- /dev/null +++ b/AspireApp/AspireApp.ServiceDefaults/AspireApp.ServiceDefaults.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + diff --git a/AspireApp/AspireApp.ServiceDefaults/Extensions.cs b/AspireApp/AspireApp.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000..e8399ba7 --- /dev/null +++ b/AspireApp/AspireApp.ServiceDefaults/Extensions.cs @@ -0,0 +1,101 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +public static class Extensions +{ + public static TBuilder AddServiceDefaults(this TBuilder builder) + where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + http.AddStandardResilienceHandler(); + http.AddServiceDiscovery(); + }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) + where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) + where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (!useOtlpExporter) + { + return builder; + } + + builder.Logging.AddOpenTelemetry(logging => logging.AddOtlpExporter()); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => metrics.AddOtlpExporter()) + .WithTracing(tracing => tracing.AddOtlpExporter()); + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) + where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + .AddCheck("self", () => HealthCheckResult.Healthy(), tags: ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + if (app.Environment.IsDevelopment()) + { + app.MapHealthChecks("/health"); + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = registration => registration.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/Backend.IntegrationTests/Backend.IntegrationTests.csproj b/Backend.IntegrationTests/Backend.IntegrationTests.csproj new file mode 100644 index 00000000..aa1fbe9d --- /dev/null +++ b/Backend.IntegrationTests/Backend.IntegrationTests.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + enable + enable + false + true + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + diff --git a/Backend.IntegrationTests/EmployeeExportIntegrationTests.cs b/Backend.IntegrationTests/EmployeeExportIntegrationTests.cs new file mode 100644 index 00000000..4bff6881 --- /dev/null +++ b/Backend.IntegrationTests/EmployeeExportIntegrationTests.cs @@ -0,0 +1,151 @@ +using Aspire.Hosting; +using System.Net; +using System.Text.Json; + +namespace Backend.IntegrationTests; + +public sealed class EmployeeExportIntegrationTests : IAsyncLifetime +{ + private static readonly TimeSpan StartupTimeout = TimeSpan.FromSeconds(120); + private static readonly TimeSpan ExportTimeout = TimeSpan.FromSeconds(60); + + private DistributedApplication? _app; + + public async Task InitializeAsync() + { + var builder = await DistributedApplicationTestingBuilder.CreateAsync(); + _app = await builder.BuildAsync().WaitAsync(StartupTimeout); + await _app.StartAsync().WaitAsync(StartupTimeout); + + var apiClient = _app.CreateHttpClient("service-api-0"); + var fileClient = _app.CreateHttpClient("file-service"); + + await WaitUntilAvailableAsync(apiClient, "/"); + await WaitUntilAvailableAsync(fileClient, "/"); + } + + public async Task DisposeAsync() + { + if (_app is not null) + { + await _app.DisposeAsync(); + } + } + + [Fact] + public async Task GeneratedEmployeeIsEventuallyExportedToObjectStorage() + { + Assert.NotNull(_app); + + var employeeId = 501; + var apiClient = _app!.CreateHttpClient("service-api-0"); + var fileClient = _app.CreateHttpClient("file-service"); + + using var apiResponse = await apiClient.GetAsync($"/employee?id={employeeId}"); + apiResponse.EnsureSuccessStatusCode(); + + var generatedJson = await apiResponse.Content.ReadAsStringAsync(); + var exportedJson = await WaitForExportAsync(fileClient, employeeId); + + Assert.Equal(Normalize(generatedJson), Normalize(exportedJson)); + } + + [Fact] + public async Task SameEmployeeIdReturnsCachedPayloadAndExportRemainsAvailable() + { + Assert.NotNull(_app); + + var employeeId = 777; + var apiClient = _app!.CreateHttpClient("service-api-0"); + var fileClient = _app.CreateHttpClient("file-service"); + + var first = await apiClient.GetStringAsync($"/employee?id={employeeId}"); + var second = await apiClient.GetStringAsync($"/employee?id={employeeId}"); + + Assert.Equal(Normalize(first), Normalize(second)); + + var exportedJson = await WaitForExportAsync(fileClient, employeeId); + Assert.Equal(Normalize(first), Normalize(exportedJson)); + } + + private static async Task WaitUntilAvailableAsync(HttpClient client, string path) + { + using var cts = new CancellationTokenSource(StartupTimeout); + + while (!cts.IsCancellationRequested) + { + try + { + using var response = await client.GetAsync(path, cts.Token); + if ((int)response.StatusCode < 500) + { + return; + } + } + catch (HttpRequestException) + { + } + catch (TaskCanceledException) when (cts.IsCancellationRequested) + { + break; + } + + try + { + await Task.Delay(TimeSpan.FromSeconds(2), cts.Token); + } + catch (TaskCanceledException) when (cts.IsCancellationRequested) + { + break; + } + } + + throw new TimeoutException($"Сервис не стал доступен по пути '{path}' за отведённое время."); + } + + private static async Task WaitForExportAsync(HttpClient fileClient, int employeeId) + { + using var cts = new CancellationTokenSource(ExportTimeout); + + while (!cts.IsCancellationRequested) + { + try + { + using var response = await fileClient.GetAsync($"/files/{employeeId}", cts.Token); + if (response.IsSuccessStatusCode) + { + return await response.Content.ReadAsStringAsync(cts.Token); + } + + if (response.StatusCode != HttpStatusCode.NotFound && (int)response.StatusCode >= 500) + { + response.EnsureSuccessStatusCode(); + } + } + catch (OperationCanceledException) when (cts.IsCancellationRequested) + { + break; + } + catch (HttpRequestException) + { + } + + try + { + await Task.Delay(TimeSpan.FromSeconds(2), cts.Token); + } + catch (OperationCanceledException) when (cts.IsCancellationRequested) + { + break; + } + } + + throw new TimeoutException($"Файл сотрудника {employeeId} не был выгружен в объектное хранилище за отведённое время."); + } + + private static string Normalize(string json) + { + using var document = JsonDocument.Parse(json); + return JsonSerializer.Serialize(document.RootElement); + } +} \ No newline at end of file diff --git a/Client.Wasm/Components/DataCard.razor b/Client.Wasm/Components/DataCard.razor index c646a839..68c080d9 100644 --- a/Client.Wasm/Components/DataCard.razor +++ b/Client.Wasm/Components/DataCard.razor @@ -1,22 +1,34 @@ -@inject IConfiguration Configuration +@using System.Net.Http.Json +@using System.Text.Json.Nodes +@inject IConfiguration Configuration @inject HttpClient Client - Характеристики текущего объекта + Характеристики текущего объекта - + @if (!string.IsNullOrWhiteSpace(ErrorMessage)) + { + @ErrorMessage + } + + @if (!string.IsNullOrWhiteSpace(ReplicaInfo)) + { + @ReplicaInfo + } + +
- + # Характеристика Значение - + - @if(Value is null) + @if (Value is null) { 1 @@ -30,7 +42,7 @@ foreach (var property in array) { - @(Array.IndexOf(array, property)+1) + @(Array.IndexOf(array, property) + 1) @property.Key @property.Value?.ToString() @@ -40,10 +52,10 @@
- + - Запросить новый объект + Запросить новый объект @@ -51,10 +63,10 @@ Идентификатор нового объекта: - + - + @@ -63,12 +75,51 @@ @code { private JsonObject? Value { get; set; } - private int Id { get; set; } + private string? ErrorMessage { get; set; } + private string? ReplicaInfo { get; set; } + private int Id { get; set; } = 1; private async Task RequestNewData() { - var baseAddress = Configuration["BaseAddress"] ?? throw new KeyNotFoundException("Конфигурация клиента не содержит параметра BaseAddress"); - Value = await Client.GetFromJsonAsync($"{baseAddress}?id={Id}", new JsonSerializerOptions { }); - StateHasChanged(); + ErrorMessage = null; + ReplicaInfo = null; + + if (Id <= 0) + { + ErrorMessage = "Идентификатор должен быть больше нуля."; + return; + } + + var baseAddress = Configuration["BaseAddress"]; + if (string.IsNullOrWhiteSpace(baseAddress)) + { + ErrorMessage = "Конфигурация клиента не содержит параметра BaseAddress."; + return; + } + + try + { + using var response = await Client.GetAsync($"{baseAddress}?id={Id}"); + response.EnsureSuccessStatusCode(); + + Value = await response.Content.ReadFromJsonAsync(); + + var replica = response.Headers.TryGetValues("X-Service-Replica", out var replicaValues) + ? replicaValues.FirstOrDefault() + : null; + var weight = response.Headers.TryGetValues("X-Service-Weight", out var weightValues) + ? weightValues.FirstOrDefault() + : null; + + if (!string.IsNullOrWhiteSpace(replica)) + { + ReplicaInfo = $"Ответ пришёл от реплики {replica} (вес {weight ?? "1"}). Алгоритм: Weighted Round Robin."; + } + } + catch (Exception ex) + { + ErrorMessage = $"Не удалось получить данные: {ex.Message}"; + Value = null; + } } } diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor index 661f1181..2b64c5ce 100644 --- a/Client.Wasm/Components/StudentCard.razor +++ b/Client.Wasm/Components/StudentCard.razor @@ -1,13 +1,17 @@  - Лабораторная работа + + Лабораторная работа + - Номер №X "Название лабораторной" - Вариант №Х "Название варианта" - Выполнена Фамилией Именем 65ХХ - Ссылка на форк + Номер №3 "Интеграционное тестирование" + Вариант №28 "Сотрудник компании" + Выполнена Миронюк Матвеем 6512 + + Ссылка на форк + diff --git a/Client.Wasm/wwwroot/appsettings.json b/Client.Wasm/wwwroot/appsettings.json index d1fe7ab3..d4f650b0 100644 --- a/Client.Wasm/wwwroot/appsettings.json +++ b/Client.Wasm/wwwroot/appsettings.json @@ -6,5 +6,5 @@ } }, "AllowedHosts": "*", - "BaseAddress": "" + "BaseAddress": "http://localhost:7200/employee" } diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln index cb48241d..2d87f8d4 100644 --- a/CloudDevelopment.sln +++ b/CloudDevelopment.sln @@ -3,18 +3,54 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.14.36811.4 MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiGateway", "ApiGateway\ApiGateway.csproj", "{A12BE865-36BB-4326-A104-2CD8CE6771E7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspireApp.AppHost", "AspireApp\AspireApp.AppHost\AspireApp.AppHost.csproj", "{B7822A24-50CB-4BF6-AEB0-7A54DA4CEB89}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspireApp.ServiceDefaults", "AspireApp\AspireApp.ServiceDefaults\AspireApp.ServiceDefaults.csproj", "{37C683F6-6A55-4B79-8C4C-E9F632B4A3A2}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Wasm", "Client.Wasm\Client.Wasm.csproj", "{AE7EEA74-2FE0-136F-D797-854FD87E022A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "File.Service", "FileService\File.Service.csproj", "{F2141FAE-7E53-44A4-8FE9-A06B37787660}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Service.Api", "ServiceApi\Service.Api.csproj", "{7A67B2AB-14E5-467B-BE6A-EAE7C9C538C4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backend.IntegrationTests", "Backend.IntegrationTests\Backend.IntegrationTests.csproj", "{975A8B0A-4D45-41D9-A26D-76DB152AA1F1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A12BE865-36BB-4326-A104-2CD8CE6771E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A12BE865-36BB-4326-A104-2CD8CE6771E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A12BE865-36BB-4326-A104-2CD8CE6771E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A12BE865-36BB-4326-A104-2CD8CE6771E7}.Release|Any CPU.Build.0 = Release|Any CPU + {B7822A24-50CB-4BF6-AEB0-7A54DA4CEB89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7822A24-50CB-4BF6-AEB0-7A54DA4CEB89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7822A24-50CB-4BF6-AEB0-7A54DA4CEB89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7822A24-50CB-4BF6-AEB0-7A54DA4CEB89}.Release|Any CPU.Build.0 = Release|Any CPU + {37C683F6-6A55-4B79-8C4C-E9F632B4A3A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37C683F6-6A55-4B79-8C4C-E9F632B4A3A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37C683F6-6A55-4B79-8C4C-E9F632B4A3A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37C683F6-6A55-4B79-8C4C-E9F632B4A3A2}.Release|Any CPU.Build.0 = Release|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.Build.0 = Debug|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.ActiveCfg = Release|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.Build.0 = Release|Any CPU + {F2141FAE-7E53-44A4-8FE9-A06B37787660}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F2141FAE-7E53-44A4-8FE9-A06B37787660}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F2141FAE-7E53-44A4-8FE9-A06B37787660}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F2141FAE-7E53-44A4-8FE9-A06B37787660}.Release|Any CPU.Build.0 = Release|Any CPU + {7A67B2AB-14E5-467B-BE6A-EAE7C9C538C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7A67B2AB-14E5-467B-BE6A-EAE7C9C538C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7A67B2AB-14E5-467B-BE6A-EAE7C9C538C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7A67B2AB-14E5-467B-BE6A-EAE7C9C538C4}.Release|Any CPU.Build.0 = Release|Any CPU + {975A8B0A-4D45-41D9-A26D-76DB152AA1F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {975A8B0A-4D45-41D9-A26D-76DB152AA1F1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {975A8B0A-4D45-41D9-A26D-76DB152AA1F1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {975A8B0A-4D45-41D9-A26D-76DB152AA1F1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/FileService/Background/FileExportHealthCheck.cs b/FileService/Background/FileExportHealthCheck.cs new file mode 100644 index 00000000..68cd4674 --- /dev/null +++ b/FileService/Background/FileExportHealthCheck.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace File.Service.Background; + +/// +/// Проверка готовности файлового сервиса к обработке сообщений. +/// +public sealed class FileExportHealthCheck(FileExportInfrastructureState state) : IHealthCheck +{ + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + return Task.FromResult( + state.IsInitialized + ? HealthCheckResult.Healthy("Инфраструктура LocalStack инициализирована.") + : HealthCheckResult.Unhealthy("Файловый сервис ещё не инициализировал SNS/SQS/S3.")); + } +} diff --git a/FileService/Background/FileExportInfrastructureState.cs b/FileService/Background/FileExportInfrastructureState.cs new file mode 100644 index 00000000..ab3c73af --- /dev/null +++ b/FileService/Background/FileExportInfrastructureState.cs @@ -0,0 +1,12 @@ +namespace File.Service.Background; + +/// +/// Состояние инициализации инфраструктуры файлового сервиса. +/// +public sealed class FileExportInfrastructureState +{ + /// + /// Признак готовности инфраструктуры SNS/SQS/S3. + /// + public bool IsInitialized { get; set; } +} diff --git a/FileService/Background/SnsSqsFileExportWorker.cs b/FileService/Background/SnsSqsFileExportWorker.cs new file mode 100644 index 00000000..aa85f451 --- /dev/null +++ b/FileService/Background/SnsSqsFileExportWorker.cs @@ -0,0 +1,228 @@ +using System.Text.Json; +using Amazon.Runtime; +using Amazon.SimpleNotificationService; +using Amazon.SimpleNotificationService.Model; +using Amazon.SQS; +using Amazon.SQS.Model; +using File.Service.Configuration; +using File.Service.Storage; +using Microsoft.Extensions.Options; + +namespace File.Service.Background; + +/// +/// Фоновый обработчик, читающий события из SNS через подписанную SQS-очередь, +/// сериализующий их в файлы и сохраняющий в S3. +/// +public sealed class SnsSqsFileExportWorker : BackgroundService +{ + private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) + { + WriteIndented = true + }; + + private readonly AwsStorageOptions _options; + private readonly IEmployeeFileStorage _fileStorage; + private readonly ILogger _logger; + private readonly IAmazonSimpleNotificationService _snsClient; + private readonly IAmazonSQS _sqsClient; + private readonly FileExportInfrastructureState _state; + private string? _queueUrl; + + public SnsSqsFileExportWorker( + IOptions options, + IEmployeeFileStorage fileStorage, + ILogger logger, + FileExportInfrastructureState state) + { + _options = options.Value; + _fileStorage = fileStorage; + _logger = logger; + _state = state; + + var credentials = new BasicAWSCredentials(_options.AccessKey, _options.SecretKey); + var snsConfig = new AmazonSimpleNotificationServiceConfig + { + ServiceURL = _options.ServiceUrl, + AuthenticationRegion = _options.Region, + UseHttp = _options.ServiceUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase) + }; + var sqsConfig = new AmazonSQSConfig + { + ServiceURL = _options.ServiceUrl, + AuthenticationRegion = _options.Region, + UseHttp = _options.ServiceUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase) + }; + + _snsClient = new AmazonSimpleNotificationServiceClient(credentials, snsConfig); + _sqsClient = new AmazonSQSClient(credentials, sqsConfig); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + try + { + await EnsureInfrastructureAsync(stoppingToken); + _state.IsInitialized = true; + break; + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + return; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "LocalStack infrastructure is not ready yet. Retrying initialization..."); + await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken); + } + } + + while (!stoppingToken.IsCancellationRequested) + { + try + { + var response = await _sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest + { + QueueUrl = _queueUrl, + MaxNumberOfMessages = 10, + WaitTimeSeconds = 10, + MessageAttributeNames = ["All"], + AttributeNames = ["All"] + }, stoppingToken); + + if (response.Messages.Count == 0) + { + continue; + } + + foreach (var message in response.Messages) + { + await ProcessMessageAsync(message, stoppingToken); + await _sqsClient.DeleteMessageAsync(_queueUrl, message.ReceiptHandle, stoppingToken); + } + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + break; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while processing messages from queue {QueueName}", _options.QueueName); + await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken); + } + } + } + + private async Task EnsureInfrastructureAsync(CancellationToken cancellationToken) + { + var topicArn = (await _snsClient.CreateTopicAsync(new CreateTopicRequest + { + Name = _options.TopicName + }, cancellationToken)).TopicArn; + + _queueUrl = (await _sqsClient.CreateQueueAsync(new CreateQueueRequest + { + QueueName = _options.QueueName + }, cancellationToken)).QueueUrl; + + var attributes = await _sqsClient.GetQueueAttributesAsync(new GetQueueAttributesRequest + { + QueueUrl = _queueUrl, + AttributeNames = ["QueueArn"] + }, cancellationToken); + + var queueArn = attributes.Attributes["QueueArn"]; + + var policy = $$""" + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowSnsPublish", + "Effect": "Allow", + "Principal": "*", + "Action": "sqs:SendMessage", + "Resource": "{{queueArn}}", + "Condition": { + "ArnEquals": { + "aws:SourceArn": "{{topicArn}}" + } + } + } + ] + } + """; + + await _sqsClient.SetQueueAttributesAsync(new SetQueueAttributesRequest + { + QueueUrl = _queueUrl, + Attributes = new Dictionary + { + ["Policy"] = policy + } + }, cancellationToken); + + var subscriptions = await _snsClient.ListSubscriptionsByTopicAsync(new ListSubscriptionsByTopicRequest + { + TopicArn = topicArn + }, cancellationToken); + var exists = subscriptions.Subscriptions.Any(s => string.Equals(s.Endpoint, queueArn, StringComparison.OrdinalIgnoreCase)); + + if (!exists) + { + await _snsClient.SubscribeAsync(new SubscribeRequest + { + TopicArn = topicArn, + Protocol = "sqs", + Endpoint = queueArn, + Attributes = new Dictionary + { + ["RawMessageDelivery"] = "true" + } + }, cancellationToken); + } + + _logger.LogInformation( + "File export infrastructure initialized. Topic={TopicArn}, Queue={QueueUrl}", + topicArn, + _queueUrl); + } + + private async Task ProcessMessageAsync(Message message, CancellationToken cancellationToken) + { + var envelope = JsonSerializer.Deserialize(message.Body, JsonOptions); + if (envelope is null || envelope.Payload.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null) + { + _logger.LogWarning("Received empty employee export message"); + return; + } + + var json = envelope.Payload.GetRawText(); + await _fileStorage.SaveEmployeeJsonAsync(envelope.EmployeeId, json, cancellationToken); + + _logger.LogInformation( + "Employee {EmployeeId} exported to object storage from replica {ReplicaId}", + envelope.EmployeeId, + envelope.ReplicaId); + } + + public override void Dispose() + { + _snsClient.Dispose(); + _sqsClient.Dispose(); + base.Dispose(); + } + + private sealed class EmployeeGeneratedEnvelope + { + public int EmployeeId { get; init; } + + public DateTime PublishedAtUtc { get; init; } + + public string ReplicaId { get; init; } = string.Empty; + + public JsonElement Payload { get; init; } + } +} diff --git a/FileService/Configuration/AwsStorageOptions.cs b/FileService/Configuration/AwsStorageOptions.cs new file mode 100644 index 00000000..0f277515 --- /dev/null +++ b/FileService/Configuration/AwsStorageOptions.cs @@ -0,0 +1,44 @@ +namespace File.Service.Configuration; + +/// +/// Настройки интеграции с LocalStack для хранения файлов и чтения сообщений. +/// +public sealed class AwsStorageOptions +{ + public const string SectionName = "Aws"; + + /// + /// Базовый URL LocalStack. + /// + public string ServiceUrl { get; set; } = "http://localhost:4566"; + + /// + /// Регион AWS. + /// + public string Region { get; set; } = "us-east-1"; + + /// + /// Access key для LocalStack. + /// + public string AccessKey { get; set; } = "test"; + + /// + /// Secret key для LocalStack. + /// + public string SecretKey { get; set; } = "test"; + + /// + /// Имя SNS-топика с событиями генерации сотрудников. + /// + public string TopicName { get; set; } = "employee-generated-topic"; + + /// + /// Имя SQS-очереди, подписанной на SNS-топик. + /// + public string QueueName { get; set; } = "employee-generated-queue"; + + /// + /// Имя S3-бакета для файлов сотрудников. + /// + public string BucketName { get; set; } = "employee-files"; +} diff --git a/FileService/File.Service.csproj b/FileService/File.Service.csproj new file mode 100644 index 00000000..ea9ad640 --- /dev/null +++ b/FileService/File.Service.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + File.Service + File.Service + + + + + + + + + + + + + diff --git a/FileService/Program.cs b/FileService/Program.cs new file mode 100644 index 00000000..5b34313c --- /dev/null +++ b/FileService/Program.cs @@ -0,0 +1,51 @@ +using File.Service.Background; +using File.Service.Configuration; +using File.Service.Storage; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +builder.Logging.ClearProviders(); +builder.Logging.AddJsonConsole(options => +{ + options.IncludeScopes = true; + options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ "; +}); + +builder.Services.Configure(builder.Configuration.GetSection(AwsStorageOptions.SectionName)); +builder.Services.AddSingleton(); +builder.Services.AddHealthChecks().AddCheck("file-export"); +builder.Services.AddSingleton(); +builder.Services.AddHostedService(); + +var app = builder.Build(); + +app.MapDefaultEndpoints(); + +app.MapGet("/", () => Results.Ok(new +{ + service = "File.Service", + description = "Файловый сервис, сохраняющий сведения о сотрудниках в S3 через LocalStack", + endpoints = new[] { "/files/1" } +})); + +app.MapGet("/files/{id:int}", async (int id, IEmployeeFileStorage storage, CancellationToken cancellationToken) => +{ + if (id <= 0) + { + return Results.BadRequest(new { message = "Идентификатор сотрудника должен быть больше нуля." }); + } + + var content = await storage.TryReadEmployeeJsonAsync(id, cancellationToken); + if (string.IsNullOrWhiteSpace(content)) + { + return Results.NotFound(new { message = $"Файл для сотрудника {id} не найден." }); + } + + return Results.Text(content, "application/json"); +}); + +app.Run(); + +public partial class Program; diff --git a/FileService/Properties/launchSettings.json b/FileService/Properties/launchSettings.json new file mode 100644 index 00000000..111d9598 --- /dev/null +++ b/FileService/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:16000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/FileService/Storage/IEmployeeFileStorage.cs b/FileService/Storage/IEmployeeFileStorage.cs new file mode 100644 index 00000000..8580a9c2 --- /dev/null +++ b/FileService/Storage/IEmployeeFileStorage.cs @@ -0,0 +1,17 @@ +namespace File.Service.Storage; + +/// +/// Обеспечивает сохранение и чтение файлов сотрудников из объектного хранилища. +/// +public interface IEmployeeFileStorage +{ + /// + /// Сохраняет JSON-файл сотрудника в объектное хранилище. + /// + Task SaveEmployeeJsonAsync(int employeeId, string json, CancellationToken cancellationToken = default); + + /// + /// Пытается прочитать JSON-файл сотрудника из объектного хранилища. + /// + Task TryReadEmployeeJsonAsync(int employeeId, CancellationToken cancellationToken = default); +} diff --git a/FileService/Storage/S3EmployeeFileStorage.cs b/FileService/Storage/S3EmployeeFileStorage.cs new file mode 100644 index 00000000..63e7eb50 --- /dev/null +++ b/FileService/Storage/S3EmployeeFileStorage.cs @@ -0,0 +1,91 @@ +using Amazon.Runtime; +using Amazon.S3; +using Amazon.S3.Model; +using Amazon.S3.Util; +using Microsoft.Extensions.Options; +using File.Service.Configuration; + +namespace File.Service.Storage; + +/// +/// Хранилище файлов сотрудников в S3/LocalStack. +/// +public sealed class S3EmployeeFileStorage : IEmployeeFileStorage, IAsyncDisposable +{ + private readonly AwsStorageOptions _options; + private readonly ILogger _logger; + private readonly IAmazonS3 _s3Client; + + public S3EmployeeFileStorage(IOptions options, ILogger logger) + { + _options = options.Value; + _logger = logger; + + var credentials = new BasicAWSCredentials(_options.AccessKey, _options.SecretKey); + var config = new AmazonS3Config + { + ServiceURL = _options.ServiceUrl, + AuthenticationRegion = _options.Region, + ForcePathStyle = true, + UseHttp = _options.ServiceUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase) + }; + + _s3Client = new AmazonS3Client(credentials, config); + } + + public async Task SaveEmployeeJsonAsync(int employeeId, string json, CancellationToken cancellationToken = default) + { + await EnsureBucketExistsAsync(cancellationToken); + + var key = BuildObjectKey(employeeId); + using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(json)); + + await _s3Client.PutObjectAsync(new PutObjectRequest + { + BucketName = _options.BucketName, + Key = key, + InputStream = stream, + ContentType = "application/json" + }, cancellationToken); + + _logger.LogInformation("Employee file stored in bucket {Bucket} with key {Key}", _options.BucketName, key); + } + + public async Task TryReadEmployeeJsonAsync(int employeeId, CancellationToken cancellationToken = default) + { + await EnsureBucketExistsAsync(cancellationToken); + + try + { + var response = await _s3Client.GetObjectAsync(_options.BucketName, BuildObjectKey(employeeId), cancellationToken); + using var reader = new StreamReader(response.ResponseStream); + return await reader.ReadToEndAsync(); + } + catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound || ex.ErrorCode == "NoSuchKey") + { + return null; + } + } + + private async Task EnsureBucketExistsAsync(CancellationToken cancellationToken) + { + var exists = await AmazonS3Util.DoesS3BucketExistV2Async(_s3Client, _options.BucketName); + if (exists) + { + return; + } + + await _s3Client.PutBucketAsync(new PutBucketRequest + { + BucketName = _options.BucketName + }, cancellationToken); + } + + private static string BuildObjectKey(int employeeId) => $"employees/employee-{employeeId}.json"; + + public async ValueTask DisposeAsync() + { + _s3Client.Dispose(); + await Task.CompletedTask; + } +} diff --git a/FileService/appsettings.Development.json b/FileService/appsettings.Development.json new file mode 100644 index 00000000..149d8558 --- /dev/null +++ b/FileService/appsettings.Development.json @@ -0,0 +1,17 @@ +{ + "Aws": { + "ServiceUrl": "http://localhost:4566", + "Region": "us-east-1", + "AccessKey": "test", + "SecretKey": "test", + "TopicName": "employee-generated-topic", + "QueueName": "employee-generated-queue", + "BucketName": "employee-files" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/FileService/appsettings.json b/FileService/appsettings.json new file mode 100644 index 00000000..da66dcd8 --- /dev/null +++ b/FileService/appsettings.json @@ -0,0 +1,18 @@ +{ + "Aws": { + "ServiceUrl": "http://localhost:4566", + "Region": "us-east-1", + "AccessKey": "test", + "SecretKey": "test", + "TopicName": "employee-generated-topic", + "QueueName": "employee-generated-queue", + "BucketName": "employee-files" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/README.md b/README.md index dcaa5eb7..c1588610 100644 --- a/README.md +++ b/README.md @@ -1,128 +1,58 @@ # Современные технологии разработки программного обеспечения -[Таблица с успеваемостью](https://docs.google.com/spreadsheets/d/1an43o-iqlq4V_kDtkr_y7DC221hY9qdhGPrpII27sH8/edit?usp=sharing) - -## Задание -### Цель -Реализация проекта микросервисного бекенда. - -### Задачи -* Реализация межсервисной коммуникации, -* Изучение работы с брокерами сообщений, -* Изучение архитектурных паттернов, -* Изучение работы со средствами оркестрации на примере .NET Aspire, -* Повторение основ работы с системами контроля версий, -* Интеграционное тестирование. - -### Лабораторные работы -
-1. «Кэширование» - Реализация сервиса генерации контрактов, кэширование его ответов -
- -В рамках первой лабораторной работы необходимо: -* Реализовать сервис генерации контрактов на основе Bogus, -* Реализовать кеширование при помощи IDistributedCache и Redis, -* Реализовать структурное логирование сервиса генерации, -* Настроить оркестрацию Aspire. - -
-
-2. «Балансировка нагрузки» - Реализация апи гейтвея, настройка его работы -
- -В рамках второй лабораторной работы необходимо: -* Настроить оркестрацию на запуск нескольких реплик сервиса генерации, -* Реализовать апи гейтвей на основе Ocelot, -* Имплементировать алгоритм балансировки нагрузки согласно варианту. - -
-
-
-3. «Интеграционное тестирование» - Реализация файлового сервиса и объектного хранилища, интеграционное тестирование бекенда -
- -В рамках третьей лабораторной работы необходимо: -* Добавить в оркестрацию объектное хранилище, -* Реализовать файловый сервис, сериализующий сгенерированные данные в файлы и сохраняющий их в объектном хранилище, -* Реализовать отправку генерируемых данных в файловый сервис посредством брокера, -* Реализовать интеграционные тесты, проверяющие корректность работы всех сервисов бекенда вместе. - -
-
-
-4. (Опционально) «Переход на облачную инфраструктуру» - Перенос бекенда в Yandex Cloud -
- -В рамках четвертой лабораторной работы необходимо перенестиервисы на облако все ранее разработанные сервисы: -* Клиент - в хостинг через отдельный бакет Object Storage, -* Сервис генерации - в Cloud Function, -* Апи гейтвей - в Serverless Integration как API Gateway, -* Брокер сообщений - в Message Queue, -* Файловый сервис - в Cloud Function, -* Объектное хранилище - в отдельный бакет Object Storage, - -
-
- -## Задание. Общая часть -**Обязательно**: -* Реализация серверной части на [.NET 8](https://learn.microsoft.com/ru-ru/dotnet/core/whats-new/dotnet-8/overview). -* Оркестрация проектов при помощи [.NET Aspire](https://learn.microsoft.com/ru-ru/dotnet/aspire/get-started/aspire-overview). -* Реализация сервиса генерации данных при помощи [Bogus](https://github.com/bchavez/Bogus). -* Реализация тестов с использованием [xUnit](https://xunit.net/?tabs=cs). -* Создание минимальной документации к проекту: страница на GitHub с информацией о задании, скриншоты приложения и прочая информация. - -**Факультативно**: -* Перенос бекенда на облачную инфраструктуру Yandex Cloud - -Внимательно прочитайте [дискуссии](https://github.com/itsecd/cloud-development/discussions/1) о том, как работает автоматическое распределение на ревью. -Сразу корректно называйте свои pr, чтобы они попали на ревью нужному преподавателю. - -По итогу работы в семестре должна получиться следующая информационная система: -
-C4 диаграмма -Современные_технологии_разработки_ПО_drawio -
- -## Варианты заданий -Номер варианта задания присваивается в начале семестра. Изменить его нельзя. Каждый вариант имеет уникальную комбинацию из предметной области, базы данных и технологии для общения сервиса генерации данных и сервера апи. - -[Список вариантов](https://docs.google.com/document/d/1WGmLYwffTTaAj4TgFCk5bUyW3XKbFMiBm-DHZrfFWr4/edit?usp=sharing) -[Список предметных областей и алгоритмов балансировки](https://docs.google.com/document/d/1PLn2lKe4swIdJDZhwBYzxqFSu0AbY2MFY1SUPkIKOM4/edit?usp=sharing) - -## Схема сдачи - -На каждую из лабораторных работ необходимо сделать отдельный [Pull Request (PR)](https://docs.github.com/en/pull-requests). - -Общая схема: -1. Сделать форк данного репозитория -2. Выполнить задание -3. Сделать PR в данный репозиторий -4. Исправить замечания после code review -5. Получить approve - -## Критерии оценивания - -Конкурентный принцип. -Так как задания в первой лабораторной будут повторяться между студентами, то выделяются следующие показатели для оценки: -1. Скорость разработки -2. Качество разработки -3. Полнота выполнения задания - -Быстрее делаете PR - у вас преимущество. -Быстрее получаете Approve - у вас преимущество. -Выполните нечто немного выходящее за рамки проекта - у вас преимущество. -Не укладываетесь в дедлайн - получаете минимально возможный балл. - -### Шкала оценивания - -- **3 балла** за качество кода, из них: - - 2 балла - базовая оценка - - 1 балл (но не более) можно получить за выполнение любого из следующих пунктов: - - Реализация факультативного функционала - - Выполнение работы раньше других: первые 5 человек из каждой группы, которые сделали PR и получили approve, получают дополнительный балл - -## Вопросы и обратная связь по курсу - -Чтобы задать вопрос по лабораторной, воспользуйтесь [соответствующим разделом дискуссий](https://github.com/itsecd/cloud-development/discussions/categories/questions) или заведите [ишью](https://github.com/itsecd/cloud-development/issues/new). -Если у вас появились идеи/пожелания/прочие полезные мысли по преподаваемой дисциплине, их можно оставить [здесь](https://github.com/itsecd/cloud-development/discussions/categories/ideas). +## Лабораторная работа 3 +### Интеграционное тестирование — файловый сервис, SNS и объектное хранилище + +> Вариант 28: брокер **SNS** и хранилище **S3 LocalStack**. Для надёжной доставки в файловый сервис SNS-топик подписан на SQS-очередь, которую опрашивает `File.Service`. + + +## Запуск + +### Старт приложения + +```bash +dotnet restore +dotnet run --project ./AspireApp/AspireApp.AppHost/AspireApp.AppHost.csproj --launch-profile http +``` + +## Ручная проверка + +### 1. Генерация сотрудника + +```bash +curl -k "https://localhost:15000/employee?id=101" +``` +![alt text](image1.png) + +### 2. Проверка gateway + +```bash +curl -k -i "https://localhost:7200/employee?id=101" +``` + +### 3. Проверка сохранённого файла + +```bash +curl "http://localhost:16000/files/101" +``` +![alt text](image2.png) + +## Интеграционные тесты + +Тестовый проект `Backend.IntegrationTests` поднимает весь backend через `Aspire.Hosting.Testing` и проверяет: + +- что `Service.Api` возвращает сотрудника; +- что сотрудник после публикации события появляется в объектном хранилище; +- что повторный запрос по одному и тому же `id` возвращает тот же JSON и файл остаётся доступен. + +Запуск: + +```bash +dotnet test ./Backend.IntegrationTests/Backend.IntegrationTests.csproj +``` + +## Ключевые endpoints + +- `GET https://localhost:15000/employee?id=1` — прямая генерация через одну реплику сервиса; +- `GET http://localhost:7200/employee?id=1` — запрос через Ocelot gateway; +- `GET http://localhost:16000/files/1` — чтение сохранённого файла сотрудника. \ No newline at end of file diff --git a/ServiceApi/Configuration/AwsMessagingOptions.cs b/ServiceApi/Configuration/AwsMessagingOptions.cs new file mode 100644 index 00000000..32feee93 --- /dev/null +++ b/ServiceApi/Configuration/AwsMessagingOptions.cs @@ -0,0 +1,34 @@ +namespace Service.Api.Configuration; + +/// +/// Параметры интеграции с AWS-совместимыми сервисами LocalStack. +/// +public sealed class AwsMessagingOptions +{ + public const string SectionName = "Aws"; + + /// + /// Базовый URL LocalStack. + /// + public string ServiceUrl { get; set; } = "http://localhost:4566"; + + /// + /// Регион AWS. + /// + public string Region { get; set; } = "us-east-1"; + + /// + /// Access key для LocalStack. + /// + public string AccessKey { get; set; } = "test"; + + /// + /// Secret key для LocalStack. + /// + public string SecretKey { get; set; } = "test"; + + /// + /// Имя SNS-топика с событиями генерации сотрудников. + /// + public string TopicName { get; set; } = "employee-generated-topic"; +} diff --git a/ServiceApi/Entities/Employee.cs b/ServiceApi/Entities/Employee.cs new file mode 100644 index 00000000..fec349fb --- /dev/null +++ b/ServiceApi/Entities/Employee.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; + +namespace Service.Api.Entities; + +/// +/// Сотрудник компании. +/// +public sealed class Employee +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("fullName")] + public string FullName { get; set; } = string.Empty; + + [JsonPropertyName("position")] + public string Position { get; set; } = string.Empty; + + [JsonPropertyName("department")] + public string Department { get; set; } = string.Empty; + + [JsonPropertyName("hireDate")] + public DateOnly HireDate { get; set; } + + [JsonPropertyName("salary")] + public decimal Salary { get; set; } + + [JsonPropertyName("email")] + public string Email { get; set; } = string.Empty; + + [JsonPropertyName("phone")] + public string Phone { get; set; } = string.Empty; + + [JsonPropertyName("isFired")] + public bool IsFired { get; set; } + + [JsonPropertyName("fireDate")] + public DateOnly? FireDate { get; set; } +} diff --git a/ServiceApi/Generator/EmployeeGenerator.cs b/ServiceApi/Generator/EmployeeGenerator.cs new file mode 100644 index 00000000..b3e7645e --- /dev/null +++ b/ServiceApi/Generator/EmployeeGenerator.cs @@ -0,0 +1,92 @@ +using Bogus; +using Bogus.DataSets; +using Service.Api.Entities; + +namespace Service.Api.Generator; + +/// +/// Генератор случайных сотрудников компании. +/// +public static class EmployeeGenerator +{ + private static readonly string[] ProfessionCatalog = + { + "Developer", + "Manager", + "Analyst", + "Tester", + "Administrator", + "Designer" + }; + + private static readonly string[] PositionLevels = + { + "Junior", + "Middle", + "Senior" + }; + + public static Employee Generate(int id) + { + var faker = new Faker("ru"); + + var gender = faker.PickRandom(); + var firstName = faker.Name.FirstName(gender); + var lastName = faker.Name.LastName(gender); + var patronymic = BuildPatronymic(faker.Name.FirstName(Name.Gender.Male), gender); + + var level = faker.PickRandom(PositionLevels); + var profession = faker.PickRandom(ProfessionCatalog); + var hireDate = faker.Date.Past(10, DateTime.Today); + var isFired = faker.Random.Bool(0.18f); + + DateOnly? fireDate = null; + if (isFired) + { + fireDate = DateOnly.FromDateTime(faker.Date.Between(hireDate, DateTime.Today)); + } + + return new Employee + { + Id = id, + FullName = $"{lastName} {firstName} {patronymic}", + Position = $"{level} {profession}", + Department = faker.Commerce.Department(), + HireDate = DateOnly.FromDateTime(hireDate), + Salary = CalculateSalary(level, faker), + Email = faker.Internet.Email(firstName, lastName), + Phone = faker.Phone.PhoneNumber("+7(###)###-##-##"), + IsFired = isFired, + FireDate = fireDate + }; + } + + private static string BuildPatronymic(string sourceName, Name.Gender gender) + { + if (string.IsNullOrWhiteSpace(sourceName)) + { + return gender == Name.Gender.Male ? "Иванович" : "Ивановна"; + } + + return gender switch + { + Name.Gender.Male when sourceName.EndsWith('й') => $"{sourceName[..^1]}евич", + Name.Gender.Female when sourceName.EndsWith('й') => $"{sourceName[..^1]}евна", + Name.Gender.Male => $"{sourceName}ович", + _ => $"{sourceName}овна" + }; + } + + private static decimal CalculateSalary(string level, Faker faker) + { + var value = level switch + { + "Junior" => faker.Random.Decimal(60_000m, 95_000m), + "Middle" => faker.Random.Decimal(100_000m, 170_000m), + "Senior" => faker.Random.Decimal(180_000m, 280_000m), + _ => faker.Random.Decimal(80_000m, 120_000m) + }; + + return Math.Round(value, 2, MidpointRounding.AwayFromZero); + } +} diff --git a/ServiceApi/Generator/EmployeeGeneratorService.cs b/ServiceApi/Generator/EmployeeGeneratorService.cs new file mode 100644 index 00000000..15b43aba --- /dev/null +++ b/ServiceApi/Generator/EmployeeGeneratorService.cs @@ -0,0 +1,88 @@ +using System.Text.Json; +using Microsoft.Extensions.Caching.Distributed; +using Service.Api.Entities; +using Service.Api.Messaging; + +namespace Service.Api.Generator; + +public sealed class EmployeeGeneratorService( + IDistributedCache cache, + ILogger logger, + IConfiguration configuration, + IEmployeeEventPublisher eventPublisher) : IEmployeeGeneratorService +{ + private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); + + private readonly TimeSpan _cacheExpiration = + TimeSpan.FromMinutes(configuration.GetValue("CacheExpirationMinutes") ?? 30); + + public async Task ProcessEmployee(int id, CancellationToken cancellationToken = default) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(id); + + var cacheKey = $"employee:{id}"; + var fromCache = false; + + using var _ = logger.BeginScope(new Dictionary + { + ["EmployeeId"] = id, + ["CacheKey"] = cacheKey + }); + + logger.LogInformation("Employee request received"); + + try + { + var employee = await RetrieveFromCache(cacheKey, cancellationToken); + if (employee is not null) + { + fromCache = true; + logger.LogInformation("Cache hit. Returning employee from Redis"); + await eventPublisher.PublishAsync(employee, cancellationToken); + return employee; + } + + logger.LogInformation("Cache miss. Generating new employee"); + employee = EmployeeGenerator.Generate(id); + await PopulateCache(cacheKey, employee, cancellationToken); + await eventPublisher.PublishAsync(employee, cancellationToken); + + logger.LogInformation( + "Employee {EmployeeId} stored in cache for {CacheLifetimeMinutes} minutes and published to SNS", + id, + _cacheExpiration.TotalMinutes); + + return employee; + } + catch (Exception exception) + { + logger.LogError(exception, "Error while processing employee {EmployeeId}. FromCache={FromCache}", id, fromCache); + throw; + } + } + + private async Task RetrieveFromCache(string cacheKey, CancellationToken cancellationToken) + { + var json = await cache.GetStringAsync(cacheKey, cancellationToken); + if (string.IsNullOrWhiteSpace(json)) + { + return null; + } + + return JsonSerializer.Deserialize(json, JsonOptions); + } + + private async Task PopulateCache(string cacheKey, Employee employee, CancellationToken cancellationToken) + { + var json = JsonSerializer.Serialize(employee, JsonOptions); + + await cache.SetStringAsync( + cacheKey, + json, + new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = _cacheExpiration + }, + cancellationToken); + } +} diff --git a/ServiceApi/Generator/IEmployeeGeneratorService.cs b/ServiceApi/Generator/IEmployeeGeneratorService.cs new file mode 100644 index 00000000..c8de0288 --- /dev/null +++ b/ServiceApi/Generator/IEmployeeGeneratorService.cs @@ -0,0 +1,11 @@ +using Service.Api.Entities; + +namespace Service.Api.Generator; + +/// +/// Интерфейс обработки запросов на получение сотрудника. +/// +public interface IEmployeeGeneratorService +{ + Task ProcessEmployee(int id, CancellationToken cancellationToken = default); +} diff --git a/ServiceApi/Messaging/EmployeeGeneratedMessage.cs b/ServiceApi/Messaging/EmployeeGeneratedMessage.cs new file mode 100644 index 00000000..e0d06531 --- /dev/null +++ b/ServiceApi/Messaging/EmployeeGeneratedMessage.cs @@ -0,0 +1,29 @@ +using Service.Api.Entities; + +namespace Service.Api.Messaging; + +/// +/// Сообщение о сформированных данных сотрудника. +/// +public sealed class EmployeeGeneratedMessage +{ + /// + /// Идентификатор сотрудника. + /// + public int EmployeeId { get; init; } + + /// + /// Время публикации события в UTC. + /// + public DateTime PublishedAtUtc { get; init; } + + /// + /// Идентификатор реплики сервиса, опубликовавшей событие. + /// + public string ReplicaId { get; init; } = string.Empty; + + /// + /// Сформированные данные сотрудника. + /// + public Employee Payload { get; init; } = new(); +} diff --git a/ServiceApi/Messaging/IEmployeeEventPublisher.cs b/ServiceApi/Messaging/IEmployeeEventPublisher.cs new file mode 100644 index 00000000..7a3f844c --- /dev/null +++ b/ServiceApi/Messaging/IEmployeeEventPublisher.cs @@ -0,0 +1,14 @@ +using Service.Api.Entities; + +namespace Service.Api.Messaging; + +/// +/// Публикует сведения о сотруднике в брокер сообщений. +/// +public interface IEmployeeEventPublisher +{ + /// + /// Публикует событие генерации сотрудника. + /// + Task PublishAsync(Employee employee, CancellationToken cancellationToken = default); +} diff --git a/ServiceApi/Messaging/SnsEmployeeEventPublisher.cs b/ServiceApi/Messaging/SnsEmployeeEventPublisher.cs new file mode 100644 index 00000000..b01cc06b --- /dev/null +++ b/ServiceApi/Messaging/SnsEmployeeEventPublisher.cs @@ -0,0 +1,110 @@ +using System.Text.Json; +using Amazon; +using Amazon.Runtime; +using Amazon.SimpleNotificationService; +using Amazon.SimpleNotificationService.Model; +using Microsoft.Extensions.Options; +using Service.Api.Configuration; +using Service.Api.Entities; + +namespace Service.Api.Messaging; + +/// +/// Публикует события генерации сотрудников в SNS. +/// +public sealed class SnsEmployeeEventPublisher : IEmployeeEventPublisher, IAsyncDisposable +{ + private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) + { + WriteIndented = false + }; + + private readonly AwsMessagingOptions _options; + private readonly ILogger _logger; + private readonly IAmazonSimpleNotificationService _snsClient; + private readonly string _replicaId; + private string? _topicArn; + + public SnsEmployeeEventPublisher( + IOptions options, + IConfiguration configuration, + ILogger logger) + { + _options = options.Value; + _logger = logger; + _replicaId = configuration["ReplicaId"] ?? Environment.MachineName; + + var credentials = new BasicAWSCredentials(_options.AccessKey, _options.SecretKey); + var config = new AmazonSimpleNotificationServiceConfig + { + ServiceURL = _options.ServiceUrl, + AuthenticationRegion = _options.Region, + UseHttp = _options.ServiceUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase) + }; + + _snsClient = new AmazonSimpleNotificationServiceClient(credentials, config); + } + + public async Task PublishAsync(Employee employee, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(employee); + + var topicArn = await EnsureTopicAsync(cancellationToken); + var message = new EmployeeGeneratedMessage + { + EmployeeId = employee.Id, + PublishedAtUtc = DateTime.UtcNow, + ReplicaId = _replicaId, + Payload = employee + }; + + var payload = JsonSerializer.Serialize(message, JsonOptions); + + _logger.LogInformation( + "Publishing employee {EmployeeId} to SNS topic {TopicArn}", + employee.Id, + topicArn); + + await _snsClient.PublishAsync(new PublishRequest + { + TopicArn = topicArn, + Subject = $"employee-{employee.Id}", + Message = payload, + MessageAttributes = new Dictionary + { + ["employeeId"] = new() + { + DataType = "Number", + StringValue = employee.Id.ToString() + }, + ["replicaId"] = new() + { + DataType = "String", + StringValue = _replicaId + } + } + }, cancellationToken); + } + + private async Task EnsureTopicAsync(CancellationToken cancellationToken) + { + if (!string.IsNullOrWhiteSpace(_topicArn)) + { + return _topicArn; + } + + var response = await _snsClient.CreateTopicAsync(new CreateTopicRequest + { + Name = _options.TopicName + }, cancellationToken); + + _topicArn = response.TopicArn; + return _topicArn; + } + + public async ValueTask DisposeAsync() + { + _snsClient.Dispose(); + await Task.CompletedTask; + } +} diff --git a/ServiceApi/Program.cs b/ServiceApi/Program.cs new file mode 100644 index 00000000..ee3757ed --- /dev/null +++ b/ServiceApi/Program.cs @@ -0,0 +1,84 @@ +using Service.Api.Configuration; +using Service.Api.Generator; +using Service.Api.Messaging; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); +builder.AddRedisDistributedCache("RedisCache"); + +builder.Logging.ClearProviders(); +builder.Logging.AddJsonConsole(options => +{ + options.IncludeScopes = true; + options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ "; +}); + +builder.Services.Configure(builder.Configuration.GetSection(AwsMessagingOptions.SectionName)); +builder.Services.AddSingleton(); +builder.Services.AddScoped(); + +builder.Services.AddCors(options => options.AddDefaultPolicy(policy => +{ + policy.SetIsOriginAllowed(origin => + Uri.TryCreate(origin, UriKind.Absolute, out var uri) + && uri.IsLoopback + && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)) + .AllowAnyHeader() + .WithMethods("GET") + .WithExposedHeaders("X-Service-Replica", "X-Service-Weight"); +})); + +var app = builder.Build(); + +var replicaId = app.Configuration["ReplicaId"] ?? Environment.MachineName; +var replicaWeight = app.Configuration.GetValue("ReplicaWeight") ?? 1; + +app.UseCors(); +app.Use(async (context, next) => +{ + context.Response.Headers["X-Service-Replica"] = replicaId; + context.Response.Headers["X-Service-Weight"] = replicaWeight.ToString(); + await next(); +}); + +app.MapDefaultEndpoints(); + +app.MapGet("/", () => Results.Ok(new +{ + service = "Service.Api", + replica = replicaId, + weight = replicaWeight, + description = "Сервис генерации сотрудников компании", + endpoints = new[] { "/employee?id=1", "/employee/1" } +})); + +app.MapGet("/employee", async (IEmployeeGeneratorService service, ILoggerFactory loggerFactory, int id, CancellationToken cancellationToken) => +{ + var logger = loggerFactory.CreateLogger("ServiceApiEndpoints"); + logger.LogInformation("Replica {ReplicaId} received request for employee {EmployeeId}", replicaId, id); + + if (id <= 0) + { + return Results.BadRequest(new { message = "Идентификатор сотрудника должен быть больше нуля." }); + } + + return Results.Ok(await service.ProcessEmployee(id, cancellationToken)); +}); + +app.MapGet("/employee/{id:int}", async (IEmployeeGeneratorService service, ILoggerFactory loggerFactory, int id, CancellationToken cancellationToken) => +{ + var logger = loggerFactory.CreateLogger("ServiceApiEndpoints"); + logger.LogInformation("Replica {ReplicaId} received request for employee {EmployeeId}", replicaId, id); + + if (id <= 0) + { + return Results.BadRequest(new { message = "Идентификатор сотрудника должен быть больше нуля." }); + } + + return Results.Ok(await service.ProcessEmployee(id, cancellationToken)); +}); + +app.Run(); + +public partial class Program; diff --git a/ServiceApi/Properties/launchSettings.json b/ServiceApi/Properties/launchSettings.json new file mode 100644 index 00000000..0641ca27 --- /dev/null +++ b/ServiceApi/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:7099", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ServiceApi/Service.Api.csproj b/ServiceApi/Service.Api.csproj new file mode 100644 index 00000000..f6c45b81 --- /dev/null +++ b/ServiceApi/Service.Api.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + Service.Api + Service.Api + + + + + + + + + + + + + diff --git a/ServiceApi/appsettings.Development.json b/ServiceApi/appsettings.Development.json new file mode 100644 index 00000000..30303786 --- /dev/null +++ b/ServiceApi/appsettings.Development.json @@ -0,0 +1,19 @@ +{ + "CacheExpirationMinutes": 30, + "ConnectionStrings": { + "RedisCache": "localhost:6379" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Aws": { + "ServiceUrl": "http://localhost:4566", + "Region": "us-east-1", + "AccessKey": "test", + "SecretKey": "test", + "TopicName": "employee-generated-topic" + } +} diff --git a/ServiceApi/appsettings.json b/ServiceApi/appsettings.json new file mode 100644 index 00000000..a743d9e1 --- /dev/null +++ b/ServiceApi/appsettings.json @@ -0,0 +1,17 @@ +{ + "CacheExpirationMinutes": 30, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Aws": { + "ServiceUrl": "http://localhost:4566", + "Region": "us-east-1", + "AccessKey": "test", + "SecretKey": "test", + "TopicName": "employee-generated-topic" + } +} diff --git a/image.png b/image.png new file mode 100644 index 0000000000000000000000000000000000000000..fdd299213c7283c8808e9c1bfbd4f20e76e76000 GIT binary patch literal 49992 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 ALjV8( literal 0 HcmV?d00001 diff --git a/image1.png b/image1.png new file mode 100644 index 0000000000000000000000000000000000000000..17c8e2f864727b489692d6c5c08197ab01d8eb89 GIT binary patch literal 21614 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 literal 0 HcmV?d00001 diff --git a/image2.png b/image2.png new file mode 100644 index 0000000000000000000000000000000000000000..cd6d87827caf78d2182f00bdbb69c5734d15d1b6 GIT binary patch literal 40056 zcmb@sbyOVRwl3PZySq0M+%34%xVyW%26uOD+}$-K!Citi?iL_8Bv{Dhx6gTF?{~)= z=l*fuD(SE0nxpDlQa$IIHUF;u-34GO$jHk8prD`trtcfz?>^LqyriUws=A7dJV^Sz z0sw%T0=yS!F#y2H+0$KJR)SJjPoEO-AH~eV!}UKd|9AL)k6#eL`xyXOWKr{k!}#A7 z{twqk?(YwL*SUT_aBcpDpuBk5hjzRC3 z(&j&~`F~(bD-Vlz9mjValYh6(r3e7vzXt$dQvWS8&jJ9tq5%NH&40^iO8@|jNC2R7 z@!zsLm4CGt3V+bk%J-cM0HDk!4sbPdH~R+zVBQaCYij`Dwh911(*po-z5@Wr2LHw5 zUm9>8?;ZUw^#9-K?*hCas^S!@d z-lyR|6YjqYEF3%xG!z2jzoh>bzt=)R!@&Ms2cW`00iZEqFyAF_nh=Mc`=y7mM*oU! zk-Jly6>PtAj`}miwWZs9y_Rf_{Q7N=$K^4Qbs_)qEVzFTEXz&r&W!OZOIsZM%Db!$?|bbvF|a zpI%YA%~ub@kpCw9KmXV&p9C=8)j3%Qc_l);a@)OJ153rT8y!C#d&+fIvDV%B{Z4di zTQjREIQe{Gjr)~3B~S#6T&9ppFk1dKAhypZt2;#w0j5KH!h2cJSPwz4$jY!|V`Q6e ze>`o|aKzYVHNNK9pcPiUhE`6iqKHfD`WQK$jmt_$Zo=M)ZeXglHDb?$MyRbdr%eF- zkdNwh-spISbvrBNJ~+xlQAz8urF<8_hNf)THCE}!IeBJh&iO|$n+JxkAhTSNS&`9w zt(VNEcZ?tW^QJ%s;?!2-KMg($*XxjFR#@r6#ii|8y|4zfw*L!IM^_{0 zm`(MlNG1K{8sneExkg$sqE|zWkB9K#lU(kyw5($71^ENLb^pxtnR}a+ahsL2{h|Y( z-jb6|{c=Js*@@YlSDs-NO=cli{X-%8ND1*=&dSzFdyjNVHpC}8c2vK?LXFQb?~`>j z$rI5>MfJg{>D8=-&9$k^54CA?Tn5&r^)Iy6dtM(8yf?-Y&NejqG9ZI9kyg1Pb7?Ef z37U#-9@+LIbb0~#$bD;BvkkxFg*_iUW@+p{&3?Wjy8OgbATT|8w>}f(MLtEd-R6;~ zv9zDBA)OCM@ogB+wNn(gsm%hnbQ4df>0KH_EZ;lE&MQZ6IjRk!zo15#<13tMwP)q! zKpoLSch2Lx<&}-#B;O6;Z0BPqP^6${tqc&nv8r*ivi4E&c;3nZodvZS6!^C>{m=90 zf2f7}@_+RfRPcPOw6@N>=u`JNW&1|*7r^J=*mv31Bshn97OoCq^U2?|Pv*^6?_1Ez zn{BAi(xfPwn(Y4{`21%Te8iRtI5{mdO8jbUSe5=gJh5LxM5b zOXzMHz2b&I?|jblIlElRT87WWss(!XY58New^$aa$LM$w|4!Mj<7GO~V3L%nk&W)9 zUc)A^{tLgo!bvZh^?hs&Ut!yG_Am+ki6*ZxEpp4u*qO@Ax5X$Ir{iS&+b?5ejOBf{ zGa80OU-C}s?`Ny)ztEg{oYekNTp%z1>R-C_l~Rvhu&VXfI6~lA<5Yv3K<5p(E`a8E zqE^q(fy?T-WnrC(yMT@xl5|{ACu6t%D_vO@+HTOeQ!Q&kz!V$9raQUs`Dh`#lBK25 zU^H29#?B_wxUDj;q;nDV*G;Xy(uWA{OU- z<#k-3exbp^#@+z>+fVssCk}0kxS!573H1F|so^#063ObmK(qcWRM|E^L&t2%=0IQ&UU^wD}!SNb#`n0gSJyD-Q57oc~-;@QSigkltrqOq0d*T_7USJk>+RqnXOD63a0 zKH9-?IoWrfnLQdb`RWi=<-kpExau`K@qvbEMo3n&S7*k*C*@21NrBdy9eI5s5A@XG z3SNgzVvthb_-x1eSpC_;mo?Q6{$*gBpLLcoy!s3ng2LzVH5hzSzLB4|JVL5_#jhOD zeLkf$HvhYc?Y%?W&INz>S@_-mp|28R4d}n(@TmQxL>$;AIj5Is;KxCd|MUS^UsCnM zE_bweEv~84=$zBnd)Ra2A$y_X#pc9+ZuS!o@o0Cl@lCyrq3+FT@FL#hvhkfofz|Ki z1zr2&f9U@oL@@7` zlo|8gy?};>hJt(@Bl!ZzH6qCf3AY4v;OJ|JX;pzW_RsBF=l%3B>w*`r1Mh3I(RP0ne)A>Q8>c zd%Lz&Pov<+Kr)8T866TWeE1x%V?N&xGYXrD zu>0~Ae-Sl!mB0!AIv(#mjD(Y)x$w-6lB$-nnlsVd;;`WQm>|eS=BE^dVKv({%88@A z*a6{IjhOj_<=>hsIeD6`#HbO^*1!Y);RHv5C)TXdKx}R|KHuZ7A1Gs>&sEARE2EVG zE~=6pqZ8aIPq^1?HoV_8x1~^{YL5HD^!(u2na4E4;T5{lGq$i|6jRKyM6UD~06Z&- z$;P7SsN@l*^lu-_IyaIrqejmJ&cxvMMirJW7ez}Zr+JMzSu5v9V8oZS?+h&02KJbc zR$-=}k1dw=Hv03NorG)86m|~M@H#l~PO&2>AEctyX<2EUnGAu6`Hr5}t$F;i7Mhu$ zmU^RT?N_gFP95L$PJag4RJ7fN8-)k0!)j9ze>&p#4eR8+NC58+&Z(0TBpE99iuYF&q}nDBP)tleZGPQ$gfBtTCvVC z4a`YxZ;_)-Hg8RusTAZ50ie*Bv;1V8U(4*ix5Axu-bf`YW;~7$PUDTN6|12c&!1$; z5hU$Ydz2Iiw46)M^A3+6L~!lbqEpsgus=p46NNpWoeYyG3sGLzE_Td+R0`)Q&ao?2$Fr8g>4p!R5^zz{T&bNeOS$Z(}GkA)8PqI|rAo^dukiiMauM!&0- z`G(GA6|mZ_P7c2Tt;|)DN2$X=ieZ;9Na6VFBZrS;(a4F4^s^Y@vETGk z`~OIqHj%!>SCgV&B!F7AIgb**H2+YYb8ZVV>vB8c}~<*Xxj7^_+a)NH5NtaqMfZKtEB+bq(&Sqa>2FkjF{ss@83cKs$>5+nIyuo zk&i!dAuM+8wBF`7(B97$z2>c+%g0&TZS+Dv_`(LUN!`{eYb)qF^wn|iI67fEJ9Ja3 zrG3Qs1nz127KEPrQ#9BccwybAp1;!cONez~h;hGvPgKP9ldLVCz@@<)SX6bD64j*U zXYJz%B|nP{`p=;?k88W;rIW%xeUx0!iTlBDYo6yf4m=y_?ynMu&Sq0%J<7=48`2Hy zG9v>B=o?&7z-1TVY^Tim&?f;M9aAk{a(BDBPZF~~etzzd=gu+^_72!g8&1pGlXAOM z{lGwy#P2KQthUrSLkPlM_k@M%QgFrdKKwbf({m7h7C?&sN`F2?Y~CEVJr#wKrd4opfolLJebK%3vm7l^^$fcqWnihf^UiGUdQ=r zX>p`SBfj&8D2bEy8D4mGI#8mH`b~K&iT6mCH(+IVBF?v&KS%}qo7KOEB^bF5xJ5VC zw7p6m=-q@=8K6v2NEQx>q3^MK8r^6sHdN{@q3xKGQl$L>?!gEzJcuWbstF>kXC z^|%f9$P3@?)OeZmBDZmNkcFUb-24R`|H@<-{s_EX(O&Ih@+%PGF~|7B*MgZ4SI6`5 zZcaV;vKQyh*ogSeJm)*|Ly~{no5Cj%s~<#ZSbDrFTEbsNGPLl*PE}@9pKtw!`oBxw zB???W3aUBfHS*Rnqp_b)J74SMWdzZK7R@9L6zsU76A zZRn*}?!oN;aN@i6vM*xQ8Dkt~?(I7`@BLi;@+l(sZr;o?RUU>u@RN^@tH_RXm$_%I z9k)oNuaLz+W)8#ir#LRNMN3XpPq4>mIQ(_Z{fi*}oI+6*dLn||B>9YpeQofKKme%3le_JwQHDhA4w1kwdGyk}XSCZlFJjirDvp?{I=Jt=W6efrV$ zZq~JRf4@`t0{`jjAn56bMh{&Wxq4zU!oR3=ntQGp+Xn3zv%2k04m14`86j#uZGC{h zr-(ElM#VS~ty)_G3nrA(eL?usZMv-y(axe4t(HkU^Fx%nhX?hn++npv)sqE6O2)}3 zH76Yr`Oby+A1-t7;rS0X<$pd_bo?M&`G|rS6*;(XsU2k&NMgzD2s6T7- zy~;pDn8+yX&(|%-k@huWg)yZ;qn6!)7_H||xkSxpoDc5z1|!PcB;)U7*5gmm%b14S z>^S4n{$bR7Qs7Q7;`BEJ_G4G{(@A+~@cVkB> zl;7De5c>nK2)o$Ba@)e%<3F}T;H>wCr&!trX(0hqzP%nJQm{))n?}*H2xg({?Antz z+>J*0By`RQJ1Gn4k32{22_&zxL{((Vc-Rp0i2Cf=iX;lyQ8SjjQMbl30%73|{Dhr2 zMqu1@F!1apSv;qCQ!{DHT@BR6Ger!p(o(Rgd}H;1K!4F~uEU`ET3~fhSaM`FC0n(- z)>l6yU7%iK=f5DXFx#&I*Ga|SoauZt`Teb@(yu%KC^JH)t-FEVStfXCY(e1RWBP`- z`DLXcqKWSYOp(m2vyL`I5^Mq2C8XL?hILMn#soo$S(WOi$cf;{Sli(XUA`>ENwng7YbWzwdt#VkuW=5 zH&GiZ+&cP>=~^S|Kk)*7nZL=eZ6M}e>a?YQ{(erK**GXl`rF%}U*E*c2rb{ntT?c7 zu_;051%blBUy1XgeTIhX!&0WSP=K}7P3Nid$A&N7>Znc{BY3~P#jPB4Y=-}YcLl*_ ztXU#+?R*Q*Y+OP5T{*iEZDpV?teH5Ig*MXAA(i*tz>yfwPD_)E69zx=upAcXz2|gU~F-x-?}mwvau@0Pdb3cN;szLi^K^KF}6u#7vaEaL%eFX%eMH@a(VCQgcTn~vj z?VbI$5OzXlvO({((>YvGvWMJKk9E2N4DW~Id%L?o9yfKZZ6_PsVQ&2Sf^W-=61S&X zWW8@AB!KMDBewc3X8qn{-+XrbG9t&J@mc@ZM`Wk-$nw>nXWru>mORzD_z6X`bh|ZA z&Q6n)f{#)8^Bs&&U0glnbnF*mel+WlJ&=yG{NZPJ7o7S&#TGs7_^?-nj30Z_gO=zpKk%rO`@gbvzZ4zlJdwth4T*$$xFf z{He$j5;h);ME?YdKaH6H?yN}=iV&&6sXiLfmL1%TK&(>K#H4^hqSzo_<7r-yOtnu85 zQ@-BJ6HKRktgu_uE|S|4MqlaOsf24eZeKs${F!Ih(BPVLj3TMssR8Yb(E6CovXLjX zF~PIRQrL15!T*5z1QS`o?7%X_FFk0xzqago3km>)2FBj&8`53EE~eB`Ep51QdHf8I zsU_R{)w-|*K)~cme9emg7LU%?t#5(G?FPbGP}`&Zbo6GM6x&R^aMJlgVpHpvT@cw* z%Kr6319!s3OUWS5DRIWdah*lNkkO-$el!r3yd{7}cv*A7aqGH$ljKH<|K5;PWk1kO z$`6Pm8NH1XS#Bg4*$limYnPhn8UG8Ic%be9F2bO^e~#fwsQS)idv0T_Q47^3Tr-sP ziLuuguD;$;qXx$#M?Y<}9oym*4xNPP=TUXgMS161{4+Im$knHG8TuS50LiZn6z$Gb z;L`0=gELo_dZ{7sl_C$`-+v*0#g)U7(%2a|%HD|+tCZF0+u0HcN)^6PI;15@Ojnd8 z2-3>RQMGEzN%oxL14_WelZta?Ss$1&mi-x;{e#1?=}70wYYHCT5-fpG*{{?}4{KoJ z$i76r_LkvExgmvtcX~YyLmIt+kTq_neUf|q);}8*wadjx%Rve!8u))SWVWe#k3CMI zxXrCg$T;g2Yn0I~y7Gd#DU}ZuxCYmoxjc8O&k<4DC$bLU&((SKnCgVjT1MZqRybrCGz6P_(OrY%4!P8dwP(LDfF0@4>f} zTyLo`v6)5ReTxMR)}Kc9OBWkhDB9s2m740i8{;=f61HA^lpkQesYprIVs8w;)T#{k z!zwJ5F3==>N{z~v`U{9`;0l)~kb%&TSIHHN^yiaL(e^8!9PNo(^w+ckr*x7Jlb7vP z1&6Ck<>1XNTDhZlW87oNhF^gDJ|rv(UL?D*&s97fyH8K_=tR%-Fd82^Ug+a;n376} z_RD{LfH{jf$oJH5FNgqDo^;>!+SYQYDz`n0C)YwMqcQnCkd|7;1d^p|RBIZBJ?Js| z++eV*?j?egzT<^@Z5`7>*b${%UAtU<9PL3U-qnC)IlN0+=`dK>h? z7o|DB_1n8mr6%{#vC%ayst7Kcrg^fcqKy$ z^wL}2zd4QL_e~2c=<^mgaRg@YAT^rC9|d6C$B($iHE(^TO#5#~X;y%zx|hBTb#IVE zz1U3UL`j1H*?wk<;OD8@7`OBbq~UfVoL0+h6!I9ka2F1whAy_fq}bA|-1xWC#Ftq! zUyuvzL*Eg6$O0l6;|l<%c@U~n9QZqOxyobeIPbgu^YN2-Neia%kyWK8{IEK0a0V>@ z#LObKJ_72+S+{imHtbTL1lq*ax7A58Tk?(KO)Q}SrCMDXkjb(v?p$r$H&0@?x4IGp zq2fhNf3XOl;GZPywT;@?oi8=2ls>1(7@oAsIFjQ0B$SAIUAKscPzJRe%f|VZd+Ho8 z<&^ZGDWl%kiP!H$v!@L_h<$G{yn~{Z*7)g64}TzKJ+&8%37v3v%ls*z)b<8mSNHh- z3@Xl4Kma8h_%GlwZ&7O0=RpiU_q=yz+AA|HXU!T%AK2Jg$C!xQ+!oM&kuiGXcM*x( zhBp3OyqPA7^pSu>|E%wOEI$cLgv$aRpT6Vn-M3``NgAV#e;HL*ATb>oIV+~3BV`$m4#`Uw0nv3{de#6Buyzsx2 z>?+M|g0(7FSqm0P;4)HMkQzEM#Jtrk7{O9WtSR}L%`s~GZ+nQ`T0B1VUg5{Qi77ef zzXY42L!6t8{z#xX+I%S?#KQf_g49YvJA3#9?5vI|$`S;Iil^*xeTYdTV?D#4ANht( zfJICGVXK=lu_d=}i?hZKrpuF#qhx;Px7r}hJHcvzMXNBl>!ZyCPXk&%BI@*IxYa({ z7Sw{zbfpfC9mhjaO9L$c_E#$aK25e3YJqaX>-Sk%oXs+n7I%6;h7qFNoOv?Up zqd=&P>>BVQ!~1t=CN?r_5W1}*pR-dN+k zk(_$!%Z%{+bZ~~Z{|eC(B=eZEOC0>}Gjzq;rG^=eUszQTx5)BR+1+1tFTP2ElWZZs z#kbcRyXmr}WtZ)^uodUlW<hrU?6-tYqaYD-H|C`kR3hS1(1Xuo($(!iU$c?j&y zJ7vjPv$fY=;Oi#)S$AyvElxHP-53)?Jy$Sd$w{DJ$@OJ{WtU9Fa)tOHdFWG^nd>9f zW*eLw_vxH}KCk$l78^d!>e1c?uL)e2gDd!( z3u``jW07M>Gqra?bYwC}<%95T_TnZJ`m%Jv)}q}0cn zLGu^DvJ`ii4?<;;M+MAs7KzoAH5B+MSHn-=R-p^rSDxmn+Sh*((x%mrpLo z<#)7c%PM*4Oxv(#`wT4o zDyInRZzxnEs_z^0* z;8+%ATi!5_KSa91X*?>Qv!s(7&3_*5$0184kpECRfU^}u(`0oCx{6m1JRPAqNP}N>GYtAX*nj5+~{7i%Gp6~zTK_}1YbxN%d zwQhFN@`nqWcE7vRQ1~Z>nTF%SZB_gAAM3*#<`D}PJ1GCq`y8>VK+TkV+ob#_3Eu!H zoIr0hlaG`%emilGuLIB$jks*39ZTc$eP@>*VVCpfFp@dK^Ue>dD|7y9WxqVq!Q}+` zRGgm9e-IRoVn#C5;w?GNv>Q847KoWp!Gq$z5Q&d#dHMj7d6J=V=>sfrHc77k(67ci2~SnB#aLe9uwF+~XnfR!&3Ada4RLW5Rks>RTbd z22*X*SYB!%RP}y3r8QvBPoB zE(z*b>3chu5(+DLlvpf3_MgO*tLT4eVE>6~G#^>~{REM6`CBbCzxfWellI-kbHiNF z7yMXYx;gza_u`6G32QMlit?(q*Hw2qd%JxAiK)IDWE{YhgQ6H0XBzI7v7y*~fbE>a zV+W70Vl~5hhiAnuykI~kz9s`A&$PxB=JnW_;GI2oRYD)XLto<@@;z*)w!U%51*M^; zt5ETRI!;ql&T%K=SKa$#B`US}w(Owwxq?V2nNP_(8F*P>N}MrklDw*XTff5(M3_XN zrKGvGHw~nIc2!LVc)tn(J7!Dc??w-W!rVK)-BL=sm$Z}Z=(&BByqC2uoH^unm4XSy)MX({^Aw6pZiDUh)LD>!yk< z>;N=cG%aGr!mn&aOAn{kMQT_=gb0kMUW;&LBorCB^4>*Q=LXwGE)o{X18Jam^XNnr zA{u&bP182T-rCs_UGZ*AoMu~0)07mTlW%EWMH&5% z=vd1YoEP-Zz!BaOALOngI;E2ZDHf3y)p74}#C(H{VLgmkcZ;T0Q{q&LDO-JGRub!y z$f_xL%4-cf5S!h5p3Rhem#rfaCj3NJqrSxg4vaa!1@0d5x>5JEW4oW-UigqQj-!c& zifi7wsq2LWmAy0Sl@*ROd65MJ8D=!MxcBQakXs{!R_ghmzW2$j9K15|gxn((PqXFD z{sp+h2yC$n%?3;P#R?@n0ozk<-;;gvwhpeBbMzR{_BP^L8x6L0dM#9jCwlGb2!2R; z>2fTY%1jlLLqKVJfP0f$O@R5J3`?smZj_4qJTmXw5(!HRhIuJQ&Q(|5kDUu8Xn#ts z0?0Tw7N$>2hNQ(%D*(T@c#n%=D@VMnWD}_db7LA6Cxb+z_2Ri2x3`2$!9m8N8vZ(l zAf2S}w4vsX^-guw&8xRykr>#d(1tj`$I)|HFvoS4_x)!!YFgelwMwQ0GP@7WNXK_4 z=Nr!q$nxi|4=m_*?W^$=QUa#&Hc`m)K3n<74as!c;$P;N=?!}HBRV}|wae`W62yAL zH9{S`ELUUl&E!#Saftfrx|DufS}Jfj#w&Q8hE+~jlqec4k}p7W}~Fq_D|Ni^Lc zgfQ!ESWml2HtB4{wI&?ZYz*(hIGR}_Ol@KAEgO@2brze*hpi;!*r;R>K)DnCDI^qm zMhLE2%=R0qzmL?`9Q6)+#Qx)g2iX}|vZkW5lQJ`#pP(=+(#`Pp4)Gb7rGBwViBY;l z*9KNFZSyx>MD$nZ@O%TN^R8CqxE02(z+PaTmG8~GS?>v55#G8j^LOJolT{&I2QhlH zBL~B@A7uMR-0>K1cqw!DqhW=7d#OcB#*ng49kh#c&V#8-(B-w_c5N?+QmKXZZGD^ZN*LZ8&_l3DQlL>~=W7fDeM#QTK40#Y-LwgvHH%3e(3P?2) zieV7B^IUUt$0wK441CwURTT@-)-jCcs}r%eP4FiYAEUfyN(eA76mH^a6piHZf3QZ# zcDH(spK`sh$ilmz*o193VIjRS70?8)an3(TT=zTO#(~+u2S6k=`U?>4Iuw1Te~*eWV!lVg{#D*HIsPk!1B(j4 zA+Bcj9uIq8D-F!=T~odX!Tz884N)ppY}1MOSPN!7zRlzWn{sGMoxI?BEifP@I7D^4 zQmZ+f0S=;}sV;~^yt>b69*dL~WW(SICB}=iWQV#oy_;23sp+E`^MO}nQwU@lhRB`1 zEivY7EnVLKMC7hMK`a(_Yo-Wn&d*yB@aj^^KO9i2b3g^6B_ItSMG^C=^q#Xtb-$L# zAc0E~^DtcZv`d-UQK64^`>CZE(GdOk%{(mNuqRs3Z-2Ng{TbemkcSt?g65z@v?*#T z5*)-G9p&Ijv>|MeLUg(vWwvc=kbX+AV_57e$4cRlo9e>&H5%TXEmz-T$E~ejJ7I4x z{e3dWS#7JGeps~A)rQbp8j=k$l#+Rdc^RPH;FK`s8a)2}*}T50kW5C}^LVsKZ<#AA zrtp>$4?3o~-FEm7XB(nU=9eV7i2;U%foQNDbH_P3w>Y(FL;4+|@fn4sO_c5N4`SXX z5Www1>QVf}Pd&j_{*!xi&4Wg&aZcU*Ujffj73Uj60kIJ;d!Y=ChqN?AlY<-(lMD5pWHu2i;*v2uJv|@uKKC~L z@z;J4oqTc$tsz01QM{%kX&hTFoQSA&WZ`@+UYiR6ZhgsPM1JKB?!=^eViut*3E<2~GJ|lR7Ie5(xE} ze2lB;*mT6n;?XR%fZsKRig>p5G8RZA^m-fb#s^9Ow2O;jG?v%HWMH9L(%o7re7hw9 za|u7!NRk;^-_1u4U#cF1lChzatQV?a-7Vu2p}bB>8r8S!LrWH8ryJAf_)XPp zHlsd}5&RJ52|mh`htSzokkr{K6}IfrT%Pgut-~U85cuit8wm+%`i(g=rmgY#PcFkB4m}5AaP3Q5D{B>wB%VrWf6b7^?mo#cKElj>K{Hj z)KhylAllQebbWAOY|-Vt7&8+XRzsF+HE&_%l5>uUG0f}#IGHtHGa8wr253>782D^Q z4Ya zZ~1BzyPq98n(GALXR!@w(JX@@ub0t}K{v zj_b26BoP~Y)vqn8L&dP^*lnqRTd~j0pq5beFP!!7IGt-wpJLXrM5QrJ^P-J5iKeMP`$p@l@Tf~O+^;~D-8({)5NP=wQt|kM z=#cq}4F6|Mm$isbLce!{R5DFH_(g)Z#t=tL0QWY^E!l6^X)PWpBxN2dxpt_y`J9oE zlGbEED;>ehLIWUMT_24ATaA_ewHA3stusq==J9|wfK`b0QG^EHvzaDH`?`M|$yVCz zf!#zPeO4Tuf{`8BJ*r_C?VE4e_GVA8O3b+Hp9O10Fy)$fTRG=lI2HV4E8o@Q>hk;7 z`)IYp2_2y3^9;_ai-Pm-@SB0pm!l{o>^U&4e&Z)$^y*fMozgw4Z)?ZPI_P|OgFJ;3O0+UUke53Qza zToC(OF}6yWwg~6i9i52}daFQ0uGHI&G8&I!I40>ArF(RvSr``jjw`4ncpPNJh#ZKL z7=RImJO0aE^zRmlV?Hc!R?J^Os-aa4FsqQh{+Q{RY&h|-#czNpP>A3o!H^$Be-3F=KIBlu5ytF1`P&H)jn_Sx{jG?oW-Pmn_yCW80*|UHsPi z&}yu|1;imGj=#eQ^*Fd6PDlpnR<9i0B1ptEEU#ue(((;|8&yqm$|%I^$_2@YabD4V zHRk-|1^;9H=%&6LDJHOBRMLDk(zK_-w0?R`gKl6&VOr$<__hHcqrneS`o! zvFecT;&~pLHB#~fCY2~hqJ&0hp^XR|<IKNt4Dq^Y@`<#H?D8rFul9LG+a7gE$6Qsqy9B+;h zeITH;6J5ka2AnQFgsX_le|YQCT|$hpm3Jb)q0_m2SbxaKlxUZQoqRPL0fdHR`GNl+ zexk1z!%Zoa6xuSiSHmiv{~dLPqQ~a`OfdxFS3n#nKZgdC?|*q7nRrx{|44km%tT$T z78BN8FQA46&(IyL*X6bTW_bv;6jQ_oE-22F$XjrrqCl1@MgHYGJKwoV70Y)5<2az; zZ!U(Z-GJS5rFw9&Y&oI<1eNiNGHG^)opd@p&*=- zgB5Gy{tM8pAZ#fSu(zrw64hOT8%=ma3-Oru#`6j~3v>}8b z5<;BVDO7I9A}@#un%k2^0*-(r2{-xPV1tydDIwg{K!{X~Ix*(0>)`PyGF`ek+0pld znwQuS#5B|}YFKjx%sEjL5w8fw!~}B(;#m&;7|e~M1l@TX{rEe=t-#vdxKXL`6gX>X zixrjplEo-7+ARcuM^mJs6R(}pTY=L2gU~=@_6kW%NBUvH4i#FuGHXZ8=|nsR(fZRj zxn9YM1U6D7Me9oVo&=(fZab`oqqDOKEUHNlgLEv|J#HPfB$Ud8un7F!jf9QwLILUb z?EZt4Xq}9+t>&3v^(FBpLj7I!oV2LudaxTUVRdNx3pfWb$0D!OK_pB-jE(yEA@PtH zO(Is@C&DzX+4e7BLqbe}iSELGoHYp>6s{s*#>|pXYC=71XoD_~R`Ow!aj&5k#O{3;Z2nKJq#Vq<3<-Gu&j2-=ubWl%=2(`Zo;3^eQ zD`OKY1t=pp#vs&OsrX`xr&l;Wjp!(}c)Fk2B!Dn{dQ>vaL~1nLW5C7y`#EJt0)tZX zdF~ZTHuyiRE9ppc*z+cFrBMVQGUxl;pI#62<4+9MWD|O8t=RX_U6neaSh!B}55y~5 z|5%?wk$^}tG-8TM0bJKMuyKV3%@ z__sJ?;rr}E^T!c+^H&H@HI%2}ea#uBr8YLv;PhE|90Wfq%g#Bh$5Fa1zJ>c9p8%f< zARHbUQSa>f5FCu?BG7 zkz?5?pL%DGYczj>-nx0&SD*uW-UKA=kM(ScHwbN#ajN?VA!ZucEaI*0;@BSqaI%^} zEk(KIosH343goGOT^aG7;YyGe%-Jq_PI&?qF??7J&~_O+K=75QIdBqj_qj-O$Y1vq zY$^h(zBdwK)50LY{4VVND<@%zsMyOOv+kOP)NJ-BdiCN3g`@9M7VBp(5l#Z!QgcHZ zGM|KKPZSFiq)FP5Js}FN6bQ7mz4Te2Wy^0^oo} zWj2cg0M63-d;{Jr>Nhr=xdTeomQ4Eam=kJwU=n3Zj8C_8d0}J~syMYkmdjGk=;sjf zu@%3*q5M9|4>8$bxBcrMA%`SRLQ|zZu5L+ceUy@3(>zl=aUT6`jr6Mh5?UFI9K1Pq zvDW6$B9eUxd|yGQnKvOX;}{|CM?;Pb;CUmD5ON7i8;hw?818$NS)?psQ)(2lnEa@g z7=n_;4^s)&#te$iWc&F3<#eQix28j{KNGvOwv8>Gw#U$0Us!+)mKpgYg2K*$0*}SV zSD+xZebJ9?({wf)WcO-nxO@d<_I;ssZJ||kIqqt-q8xpqCJ1quI*QRY$)g*X==B@6NP)h6TfYGyPU5oT#fo{xIySLXI zd{4?KW|zGFcHS_(WYqk@k(8z&a&p%$b_lWR(RoM124^O2!p=}-m-_Iod~{1&JoLP-g#dB6`7KGE2AJq!HnU`; z<0s_yl-bBp6_ifrx%Z~GbxYy|*-)s4ap(@mM-sip5o2N#1NY7%%-MiFBD8G?NLl<0 zC)COnqVO*e8X$uv-T4XkbDk|MIVc_wipNuZvrQ&Wg^MdQ4#DMwVx*M5_u&7DqqK)m z1)v1FvCB#SyvwRhdMZz?=ah|o2}z=b6B973)i~lb@if2BEuC_yKa?wzrp8{K90shN zeMdo!-5JeMb5S*zcfqtJE|)SWL|} zJu1s$qX>(J!)iL>Smf^J!F|ZY7-w`sy)-}jZ3~7hzwD+M1S5TY^VsDXj&g zK-s7yL0Cc@AZNwT0UNYfyy$M7)IlI6g5Za17H=DaO#tI(TO=pJaYeY?#-N*W>82go zu42hLaYE-?jeX2)jJ`F-=Tqo6BZYuBN3jhZ(oZ>Pxjiy#Ro6euc0#9LKHBUzP=$yv zK;aBX=F}7;l`#$n(OK0PGkH%Hr3swF>(YK`tC8&EH%Eons(g4Fpe95AHAs?TC%5YX z8IwJ<+(3&Ut%13VaFn9KyDu=ec)FB^Eet$lcb6f}<{`#HXE)tkX05EbA+mGfmnpsF z3-J~QIxY3{`al~yB8Lhwj zVsbTiyOC|%P*}Vkmib8@;(mbJ*6uwy`BIC6ta$abSig=@{WLqfegE-#tt$EPm!U^I z^wha%Sk-d)Mb7EpDa)qvMwj#lh-p;DKo`b}Y8wgqSt&l$dQLqs2T@0DuK}r7%F+7h zsez$@CPPQr34MuPoVB(Fg!xGc*mgVCC^x?)C%@}+ae^{KAxFLeqI z*eiydRe(a&vER8Rn)=e(i(qcTxFDW~a?YHlKv?FMwl3$GLaPb^e2&X46z<=hgzC_lTD2+Vwnx^;EP!E@BsG zrV)({Od27CNWxT$V#PaUcZ^)#Oe)Aya9@q`iPNci1#vQVfB$uq!WY;qMb0^AD1`FH zt_d?9Cko_?#V;;axP0PBj!8t#8?L1U#};iH$SSVhc#s{M`O-a+%IJ8RuBn9yCM4GE z@DSNRQGFrB;2$#9T$PQ89k*afD#Mw)tv>XWBeiFVtAl^+6B;rIH{3@3e+`!V;A44* zr0@-4e-90ny76nb8z$|M;5{LT6^ZdOihi3q7)LiYVyPg zznS6|h6SvcS)R}_GkX5PaRMZ&-BIej)7B_onuMtP&Z=Lg6SfJ>I2uu0Xwr$(C?TIFu*tTuk#>BR5Pna$OhzH7uis}B6BXr<-<~iE-iP2A6^W_C?oFtOOt;~k zI3P{n_dh`JF6;kN024}**Ow;XC!*iSe-Hu{(zmhmKYg#XZkhWWk!714qSA|bI7!s$p<<-_=A4a5g0crEy_OtQ%#&nneBfD^-hpsQZN z0{cZJwCfP)vzog~sxV!lL*x0WBmNaKTs55G`?*a@$W&I-3Bbo57EJjV z`Uglf)*Brt5bKre$&a=92l%TSc%Yf2A}o-QK>yCJhk!T_U>&&@3! zzK=XE#XMd1me>^pVt~X|P!(Ms?gC@s&2t3dk~!w({6N@t4PF7s3-XM~5d21Kf%(F_ zg*P*4AEZZY@yV=qJ+fSi3k8lD0|MGXaZ65A9W$2h4AQnoZ#vMd_#o^v3Zk-X7L1XO zG1ZcVL(OJ5HIVy>zmg!tbP(Oh#>OdpWjpn4qpa=lO$Q$-aFP!b=E5)!i7aq8)DmjE zSU%1?sNRV>G71e)M<4v_QxRM%YOU9t03xvDPhXI|Vq8aLApH;Ev>gcw$y3}VmtJLg zniy{kMYQ>X7e8%5aSLf)UkISrBGzm(aPy*!xa7h%11Xm)7f-;=x?{FXNCI! zE9q*Y+b?f0NrD`=g(9SvX2u>;@jwt_P^lt!WDwevA7mLUv{c*F0e8&u*!6Q#v#oga zX`qBXe4hVNhM2gcjV~OWXqZN#7R*hWrO=3op6NRQM&&`sFJwe@kVV7arVTTmK+R%q z&hmp*ij4}oRFp$%$R_27*ccK0*r4nh=YSiol)aeca(;(!1YV?vj?(2Nq)^q;){j$3 zEtE@WGX7!cepwUqFseuCqy5PAXrVxj15v_uAI?rxzFP-FEm-ge?$QBfBjNI1)xwPx z0>ovr?({Lg8I};pl`C;LQz=Ax=@)^0X>oSbN8>^SJ66Y~z9O-kP^h2EqteX>7){lr z3$y7ZksAEF@Hs|RiDJOq0Fv>jWu6bSNf2BU!hFnD38Ixyn~0gLVys5wVTwIjAVcN1 zy&eUK+dqx4f7(#gUY)kW%Bfu>jRU$6pz4UHW@-M83}3(`GsT4Vvzj7%*9m@I%WgEI zEwC_vNtKSBcra8+T^A2&={kKbO<(4+P^Fi_3N`#2`V{{9ndYk_M|WzUt*sU9PlbWK z09Lp9TRW8QU5)JOk5}~W8LGlXnAWrCIR}sj@iMsR?0jAmDC9`2T3T-$&GsX_|4|0V zSlGsnT4Z9NxPnT8MXJHXG4dWJkYxoHoLTdwdYK9zsA%N#f4*;TRzKxr2{N__K zU%Ltg-Fm3+!zc{fz*7`2Wlo0sLnREcfhA;58yLw#q`jVKnNpFjg3&uP%7(%%ECchU zC6E=!NLPV)0)0Zvo_9g%c6++)H4w}`ke?aZUz)=IhUaLg`3DQ(6dJR~ zKS0M8MbNvRPz@UfL{8{_run4C1;^}+ivp4GA0*2~U=UWJ)1>Wo{wRB?3BYv!^ci)f z34R^7UVLcIpQx`T>MsJ7qG)8FT-CdAuLRI42KwD}Kqrpq_Bi=ds|Xx<{t3&~2$8K4 zKXaH~cmuGAB}*Fe!mR7=Yg5Dq{?kEse-et@%KN+^shn7s|;>$aO3g@VS5j-)M3 zf@dX#*cL~R0Wc^-2NTvsp3-V6*iSzEiy>WOArccESlJj!14Ga;`crC8C4j?yKZXG6 zSNv-&6D@3y0~(@u6YkK2er|lBUo`~|+pN*Vc(vh}m5@3Tdnm>#9_`qgvYK4*CovOk zB*aR+!D;^8wuooIkY|H~zfy-rHW;gx5>X8nDQX)MuOqzM@(TmaSF37~yv2N_v0^D& z#}dvri{GEf`b0I<0Of;A5(N{@h3S6w*{iw%fW6Sqbo0yR<6 zn|x>Au1ur(wXBU~BxPZ1*<7i?E@;tThx0=wK16}p1py~HxRS+h2~1|h`oj2N#^Nsz z*n@r9JS4hsv&zq?mo-QWtrp^@5^3?nsb1cto-)!2@>nU?3khjY-E)FhwlLTvw|v zqTp(GWxev2RL)5;-g?LiI1AxFB4dMY~|a}z<0 zbPKtXnimrTbrQFll%Jsc!>9MnK&uGNr3l@zUI17rRn{sB9I;nrzbks1sQ&>10Yv3t z^(y#oYHCbmeG6qn;xgT| zfv}Y5^b8!QFLG{SC65EM_B`cRR02YhZG(alh{k+?ARIVltjs%p?xsf}Bo#)3?|L!D zS!5Z65I?>dDG7T|6<8I6YWWXLsJEmPk=B6x08oE%v_6ay8L5F=JpD;2h;XVsrB$#* z#-vAi~sqZn2YmVo!ARht+FqiKvyyP-MKtbI{Ng z5?dZ%e?t^?$RFCw^NCgww|>(Ay#aM17Na@(rr8lEVaU`Qq0IjPPkC(-nQPYbgaUs3 zK~q;)JOZnXzcGFt(&SYd(M%T~sRG*cI-kWY+wCfU=77+H?a9KmGTy{f03KX|HTHo* zxRtA}qgKA<)@R3+EV>Us;exbGaI@M!Jh%E~ZV~VyF6er->0)>WTt7>NnrCuA9$BsS z&a^htD~cUdKKnXdqUW`$rZEK_P?bVa)+#S)l-A3)K&qry0x2S5wR|H~Z zQ^{)mq}WV3=WWpXZf5RwF^1FLp>%@-SuzWh zmmpN+39QGiDeo!u`YS}Gh|Km9Wav}OR>VOxP<8#hz(G`K3H{TxG2|wWmf$|yK`Et} zfp(t;6LB9~PQi}SXv(yD-SUawEqfFy0oA{y>+srZ(bC0y0Zf+6(nB;+n1@XGJ?KCs zbcFK%|M^{rm;$_+_dVB$h&S{yOix^`(uzdWa{pek{<0AJDYR0jk!cx*F$aCH03dTT zF06IMsejn(F4jOQoEU{O+7v$0qT?zez}8t^ujlK}>EVk9DQq4|Hx3M4kEt{A2v6V; za+0drr@-!`_vf=Pc{whOe0_#L1yvc^wxGvS0=6ePF-@O!%Q?Fa$d#;G)Jr%3qT7X9v%X&PHnWA<#j4&ew{SA9{R1;F`cd}x z=UZPM@#Y3!@(a@cGBuYG9}PYAfyia~lN<^yrOd!K7lLGprT#G9ZDa04H;u*rl zsGx{$H4I~VV@G8NE+!R{%jAA@;Y8%;Q(cnE5} z=&?CHF*(ey=LavvRtckPOW@6Ygl%o2iGRVrmBk|a%llKkzE=rx>8dN+{s_^FXH%B* zuUqjTW~?$IK(Fw@uB*3E5%f^9Y%S2=(7rycblYJZ34+pwZGAE+XW@kwpZzA&x_3dN z+&SRU2z0$+n)g2lNuuU4DJR^xn{X3Hj`{evZj(SG|E~SOi z57m3up&BeVOnypqXnt7M2d;tx=V>f!k^)>bxgq)+k#mx{t}&I7qyls<+962@cIwEL z`b(G0H|%->4VhNnP_>tdZvMO!bhg{vB9KlZLnUn6BKOURtoaf?|6a={GO3gu8jozr zji81ka)j><7>8R4v_mqth9v@B4n+3~IdN-r&s_{iRl*6L2-Ze!?S4 zDt0qRuKfoveJ27pI%R;y1YoBAG=V#f@VrDIFeNrx0^x~4poO%UBZ{G2e@zsS4H1#O zT>+K0*%Syp`cXeHRi8x-rL;9ng~fCUr#0AO978o1Ep7NW-6d0PZljSrWNX^d?4yqq zor7yGaGmIOP?}QZ3E4AP&R1N^yl^9cwiQNy(;~kvm>Gaoyr>8m({2Zz$wHQ0Oz3vM z)szBZ-$K=H{XtM%!Li0fbdpi(i4#w-JttmgHiVg^CV$bH}_PCZ6XEm(7V`ez2R!kBgn(rUC@{G-%HnFJtOT)iM1nA}?)3FlVd~WVQ8X0?|Q6OZf;7HMrR5sVaVFP%$SR(6r*N~r4mbh1om8XL(P~`svHlsu>>d7I%yseVE9X`KXlC1Osm8A%8X{I zCqW;o34~pT_7fniiBDiPfsZ+fiU^9)NV5^N*osbpVg!Cc#57QqF{FF`R@GtBdhjlh zaxNOh)E4T36N&9(uk26kmrs`eOQ?wYAp5q_id$;jFFOXkkZ{2Qd{$63dVmgD$G| zCeS_Unfmmg${_HB%n6uyd)BOK2;ffzASK9(PH}T0%=nr@FoO)Vzk^eR=rz&PsC1Q* zRgFx+FWFss4_)p#(SIOnb-YRclT=vQZvLFw0S;s+$65 zFmYL*$Y7zvD4EpGz{*aU!sM!0Dmf72eCJAj=q*}LsRiU{>C;)J`$9B!iCPZNb0)Mxf>NlC9?*CbTIr5G zqiso9Q3)*txsHbqcDKkg8S7!@)#;vZV_>}l9PO2J||XL@?T zBsgZMg60iXQRUhVhTbi{0^Y_-;-O($l(CFM|?f??{~Sf zEI$1j{7>>xvgO~=!CL_@l9WarqaybBul9dm{K+Yov}6K+-SbNG66TW*LAKD=)WHql z!wVA74{cjbiPKceLpk>mLdXE=203`-FaHDyV!ZfkS`L<L^tx7`umR+ZnBO%Td7} z&FPlG5=4bQ?6Y_x`=A%+N`--^`ZCC+VEomWq{8r93ySWfuE7u#1bAJ9``GgBEAt03 zrQvz#{h*v7VEP=q&VN-nVg$R1x@bd$EA6u5-nYEbXmIJC@!9l<>T4@kmXlsY-Gz#1 zw9Ue9^bpAlW>KvlRzL%4$V-|sKtN*m(3rQtK=8DFxg`eP9bqvg($>^)Bj6*r`v(Xz zz*Q;?T#8#S{Om!Oj!HEbEX<%WM41j>9xzWH!~Iz5`%~u6*&iP~#;o3gg#!s6cP#$^ zhf*5lj%<+*S1cqJgJ*mAT{>(O7#d1cCq}C8H(P;S%o>F!3#E91oBRjJKx!wl)JDOE zHi1f}ozSo@Tey$5-2ejzIS3pE$-{n&v}NYEX1pl^XF8g&>*gU`Xlur7GZ9lGfFJOJ z-bB%}YGHH8=D>hq^QkutE4ucgmaG)m6VcBnf_@qFgW2dXePnA#3t<@4iYAGHQ3?2| z1~;d>m!Dr)mIf_l*B(LkhECx$^SZVMNI)9NV~3#!VS9z^fO+*5$MuXmhX!Uwbotdb zS$B4exb;3A+v-a8$W9%Pg9Mfuu^O4V$5ow zcN*k!7~VGp9r<^o@YsiH-;@GrSr5WI+->sY(pjMG4JoZ=z?&@5p4`J>)eqZi4YWH*Yucw=@uwJ$4>x06cQyOE@Hk5_V96f~LUz z<2l@uc}mvVG`(@G0AOvI3lKsgWyzP_1A}pncQ;8aZ~N5y70j$epQ&_wu*O_R#P1542bc4x(vnJkci5uTe+sYRFj&xk zmX_KPf`^<$5PQ5c~FA=vVq#MzuGMp+!D@8&I#C zhsht55BJ5vhvnogf8OzYEy%>Zp)J3TsruuTeyU2pn?@2@AbBqwXH?$o0ZH*yVGhCc zHdDyCdv*0_*ce||%}+>zO72i9Fph{jD~ub7s!Ak|RpNJrU<4P^Ei*eY6WKCbTao$$ zMSIJf5VmhY3oI>8xHyZc2?`To??o-o7!)|2zvv^gN}}5Gf#eH8R>T9t#R^qBr@8Pz zw-FxrxRH;j(XX{5zX~X~$iEAtBwEf5_;%PCRQO*eJz5=qE#e~bsYo#ing zl;qV+7yyvXB1k@)=1f6{aQ<|N?R5b%{vdu9e=XXKz`^?A2n9R0-X%qmxHNu$S~7(U z#^MQr%V9{qca|@bsvZUW@RV~5@Hymw=gl%=<)WxEinXL+p!&0boGb2+{X#)_c+h%U zOi-XM5(kI>>Y2gDW9WUEw7UZp#TB=T>oKLb> z@G+waruP9{=we`yGq5#Zq{P{% zTOxem$Cvy$mKF--7tA*U6h;1~f8SZ@kYG@t5Fk+B6c7Xy8Gu3z#w<+oUn>zyK?5qO zh>CGwzvDbO5^G|i2^x{AQ_{kok$oc>o6^9|e+@>UARvN(s(%2aL8q2r1q)s-o;K#3 zoJ_UD5nb=>bW~#}t}gvvF-gU;SbF;mZ8t$uNy%B76G>4&E#PFE{5g#7C%<2h6av|I z%|ILM!BTZG^`#j`W|Axyx$xQBaWG11Zcurp3{>7dE!bjjG=ipLhEMt#8Qj+`zH<)+ z*BSU2Tf*0F;%DCM$TiQFJs+Iew=95ehixy7{&HxwAwd6AA!Z`^FUuLINb8X{jCOyf zGS`BeY~Iw^=)uym1KV8-#e2`1Y*T-tMv>!N>*V{PV%PqNUPPITZRjhc92e?w0jAyX zl=Ucd^h?N{$ysGXViz}*03v+dRFg!PHCFYZaIgR3r%qfAR%xmvgM%N}th51p5BX@6 zUMTVIFC@W3qb*YUniR{&-Op}E&F3XMaOkdcZCppg@X#~rum~9C>%`5dAwG#-OZN)| z*`|?w$+8Ux+99%J57SO%(#fG)$(L~$0b*oZzkR73$Ts2fn$s5{qH2zlp*~gBSH;W}9NO1o5gXZ%(v{ zf>4$g-FDaY>pH>yAPiEaEpUu!PAZ7J+L`z9z8qUM5jAs6JP zc;aN%$psfx>df@RtMG)W{sUwOo968uuwk=xpQlxd-h8f|c|?)zl9n18kI!Q}NR4}C zjRBMUW9TTWKN6!Pdk}i?IEy(II^(`$D3|Uwxk96bTV9ST=XwqLR2h?5sv8xI9-BT{)$j-EV%Pk{OI+lcuZDJlFTv<$wM^jybP#85BpqJ zzHHJ`^}(E+Rw9|#!V7yTBVhXH(_!PfWVc9x7xn_D?y18tEAb-EZwF+Tfsl23rtyj_ zXa`}oR_gK=)%~fA+VM*B_@KUf)#n}KuhqVV{?Tk5|4rJqXvBgFqJv+wOlsbn>1u!P zWmK_d$?JvHo5}4^?7?b^jqwxOHd-C=m(Y;(>AB{fx~n_(!iVTGBX@nx6pA(eL7*rS|mR#~Pz*IANdJa>#c++Aa2xK~upgSnH~r+eNT5 zc0tss>uDHNQ8@l}ceHnI{P&O8yWOUIa zGn!r^M_MZDnu=G?(=_)>dciXFhiX;1s(nF3#(`2XY@wuL&-)_M zis0npnB`|l)r-p*Nl}UjX3S1ybN)mbja1V2qB;_mn~hz-poaoq`cUe-((|(^ikN;s zl&?7lpYq9I6xkiShO9APKI0xz~~F3E>3%GqAr=y%YN^ z0Vwv|r4#S04R!#TooCFPv~oUneAvl4zbe(cN&bg{chYy3;bLLK&CmY1l^uq%*v+Te zumx~yGT7sQ`&A+sP1c3-(@`;O;q@=3tbW$OlbM*U?lUsO*52^G1DAcJ8CK21QajA~ zUJ`t{&*bAoyBL1rHDl9I+EH%i=Z1Zswp?APj`z+1!r}mFPlH^Xwfl7&`m$H&jdAK2 zsi-fE+l7&IZVcOH)67n;M|pWSKKpxRPbn3uA075`(+GeWf zB$!-SO9eLY*t%^ES_mdZO=L_zOoe?Su!7lFiV0P%6XwmLbGpHnCx{6n3 zu4B)s#!H$VDsh3MqvY!u1TQGP?1Is0zd~mV{7AlhSbQSDR^W*AAHvE*a*oT~bNjYx1|Jt%4-ca64e(1F1hu^>wU$ z)D22996b0*a>m+mrL^V3tdF$dGC(GDd~6#TD&d!+zK)|C&j)6afu>;%Wq+D~(KL@M zGceiOACP-J>=RMXdA)Y{wl~u}BJxh3HCD1 zYMH-jw98cWCm=s|S!AFevmcaU%88$Ij}H6|>aBZ~GGG|F5a(H6T}4Z#y6$Ar1_K_&bVZ^(GW>D64@Y?^h-B zwNY0p*$6TR_B3g@iexerHDM&Xpwq}=!vBk3r(5GT;>pINJ8`F(Mfcsrj$>P{?vcJ$ zwq;uW$nA(y#k5wX`9me)gi_^wf2;KCXRpRByZXbgz1zc1In8VryHEAch8-dsAo|^? zFChl|t%&mT$b75oh`yCL1-t?aVsQD3d6GnIAY~xT$Nr{eOAO3j@AIh7%7*NXN!eNG z@|R8)Z;16&T~X+i;UvFgjs~tJm@%DomyF~r3UITO`dtk1m5be%L_k`59^+LrGz+m- zS&7!dkx3|J(~0cix|g0#70vs?Id8{{cG5P*&fnjz|nu$u~5sxD`IkH-i*# zAwL>+HG#-A?a#(A`U3ZMA2S~0$r<`IJ=GDPCgCLN&;yJy5 z%V41XS8C%thZExEaWN^0?Gj>H%hL~w25W2)3v`Caz1RM*&j;P^Sl5vFXkDENWSEVu z%K;*%?}k$Ze3XRpr2;ER5n9cSb|GXsMxaCA6Pp zNu*hKLv2W>I6jQ(rk!#Z8%C5e++G?|OLnuWX9lemiU?sK=flecUbe&{mxXr%#}8f6L51+6kth z@q$Ii6i-Ok-syRL^60`Pa1cX>c+0B;#lz&uwFuZ&Lk?X{%d{Ll#VT*cSR&A?*j`zh%2&J_2Tf%Ix1^ z@yi0;kDw5kU>Bl&BNAP-(lA(Jb5m$~3*xL;$#!b(TAxOm;cX;>fCud?U%X_`iPZcJ z8p`ntqSE@Cy)%N4Np?|AkI4sJ&5pLaXl%PJ`T(2Ah$)pk@1h1>OB6a$34XcZsO&+erj^Qc-wGoopEPeFNm8Faom5GOk<7EivTR z9g*u`so#rLD?G_}M=_t7%VP&(3r-R!)?jKc>Ro32WSR#FRTeMJAv~Nks{WJ3FtEER zrsTgMEyWI`w;B9cNeSar)rS@**0QeNe=^KMO6$Z-0(Tj&69n#~upKFKmOF3{v6=oV zW(Mo`Vlvm^k+AM#j>nZ&j>y~`;=S;3^s-Fj+*TVmB|v_GDLz^W{4vhd#%mw`IN&FD zUwKI*pm)Ry^T`(awweanN4K=7m#z9eJ&qghDt94~+}D`?9`_}|EiNS>BbV%vwPi%Q z#rZ5b87@9LIF7rG*(C_M^ot;JA;4{6G-e?jidv+-9s|v@9E$O!2?RS2^2Tbh$^msO z@}OK?Da7lErdPKa5LQrh4Bn{4^W zcz77Oe_O6Xt9%zg->9E{i;T~{an|+wUQV$)qD=}3cy%}WiNF@?Amit**&-#dj3C_b znu=35)Lmb7vZiGAiotpK>o=FU+TqR6^(wAU_7a}}+rf^-O%w^~afX}$0Jpg1#8R;q zId12=VIA$2jd0uMIM%B7Wmb8V86fNyUsIGyqg$Iq$90ckOR`6~1MN_r8A69<_p2s#!DMN#@pJs=dF)Vb=2|YLU#(1G&@^RhG-#aq^t+6WOjy_H zzS@66?pA-fln`(j&kuh>OYxRS-X}n87jbeqW=9qyw7T0Aw}?eod79x%PU4adC*6ve z8WOBTXA~$d13vdZs;FOAgoAT&CWh1Y z>STh?PT}`U^W}N-D3v+VRyyDxpyK9JPWAd*?-N@kh+Jl{HYaW{RZKVo-ingwNyuBB zR7%#t<99e8P!&OiNV8j%85BZ5S|4;q(Qxruau*!lQ$z5at=JhwStcl{VA(@E2*mU) zf2XjzK=~x=wWi06Gut<$l42Z?5`S#s%5%YP`~c5zN7m|Poeh(XgMkWW*~AXYGo$agm8A& zB!S))^+s~nXrL_fEg*ib+$F~5F)1ycy*PigYn`N9MBCCLe9Nvc)f2LbQC-l0F>p(@ z*qgl>4}W3lz07(FE@hp2ts#h&Z9*|HYN-blCQg`O+c1>QoAK#NvQZi*ImwzVr6aF*?wK< zMCW??&fjwxq9$PfagyjV2!-y;lVC|JmUWcK7@@(W;JAR+J=I_|i4cfS(Yzw{<1dmYp0Y>A<%d75Sq+j%XERX`zQZ4ppiy;KnB{g=;i7~)4vBeB7#WO%v=E!AK z=MC$8dbAf{dP=Fje`Oj6kh1E}4BG5afChSHYFbJlBcp%cOIszB-X6Tz3W<-g;Nw8Bqb|1gv} zY1_&JGy4cS3m9{`+hoX!Q8b*!4_Dh+lz7q9qa51#$!>k7z$|-x>H*#p5y%LQ`z}>s z5OF0nTs~u%D!td>eP0(EW3s3Fu3>4Q_eK!hn;ofm;e7oRl8aaQaSiKI^vA6y+FJeH z_yf)9$C;*EvZe0M1QYo>N^yd9j@6MZblP75lZmE(0N7q^N}nE_uU9XJybl)N4o0?C zlve96d29?6Yjth3TWpm#y95m;Q%keO9Ap00Y?Gzqr=+wLE*Fq`nlb6wsh%!geKIrx*_HOxoHnCq4-3%*cn;}m4i?MYI8D0t z9;u&Mb<7}SzAhTylVjS&X}2SZ$G4;htL!_+59`V$I-qHnwz({g zG1nzw8&r6?JE(swGZxiZLCkHV4*NHM?i38AQiokT{1!>LYsZ5V;0iqdX+7?Q?!ew|Kc4cbd@Ae)ETvffCoXixd2D z03l!JYSACDMEA3+FV?%4R1bagp|KhmiVxE1ukBa6^`^?wq?LvZ8~J|mTNd40Y_}9f zAjh~j{&3dI!?@ltBYSdCcUC7b{X+n!@^w0w!YD88-Zj z|0i)mXm3-rtO)VSX%RadS?rST7J01d&*h_ogiQI2scf?&subUK6tbTsYtgbN%!b@a%~@A#sT_bu;li}Wtn$y)QK&bbgfvVUQCSxoEp)p|2cHarYXqYg@z zyi7YKnqM-WI=|+Za%GM$UTwu_2Of>#po%JaI>+x>bURdUm=kt*GX$126i?dS@D#%z zM?K)8EEP3#Wo_}+*%Xzj-6GmAW5~&)DWOJKX&PEHw>zJ$pqr1eHK}0QSP+35U)p48 z2r`${x{PNW{9vRQQV{Ls`}B5|8k*uU>?dw{D3k|KZ{>3W>whA?I$Hb2F}d6liiW1; zXAINAr9W9B3f}f`0as8%0phP%GG)}m6;EzFmQ*7|B7Bil*Y7NG)&tV~zj&j2on`Q% zE&RW|iTm*{c9W2$&rdpENz$1z{=f4#>Dy&HNM88(AAix~776jIFyuej7u3@MZmQ*l z_%}2w4Jw}}Ol7UvhWu=*?Gsp)p09UUmU3U_hl<%Hc&yvL6?xnwr%4?&k13O?_oKdk zX|nqWMeF0PwVN8QFAlf~hll0+YO!nNo;!-4yje}vX1g`)COzXXW-#~%pN+c1rjH5V z<$YGu$Dw7G$rQ%-j{RK(jV;r{=ZESSv$CY^#L9hxLF%|5{$kVCoUWq{#i^C7A+}q| z+li86o~o^JxU$}1nnbsPVxNYTb3cQ+GnG8A3DfzN;EyFs0c~Rkf=m?yP|G25!`4+{ z$%f2C$26@ORzJBQ`L`WnP-fS*zOQ@~&WpWDmNX1rnZ1oGk?3-!5J#!gyS;!GxMo)P zE0()tS3D{%i!UU?DdDD#n~O;$wU)AuhM0hDtbVTn4a8cOOR00^rV*6&R}II74R+0C z7^^z*K5&Z-Hg~MdqEh*5H(;+OasqwzBJyWr!-In@&$U@XRjiiXGe}=XCw{Z!;2=V-NA^iI7%E1%bO9IXNYl!t!z7&1kS486ugrn zc7+ZmyJ&6C%Sf=62s+HoNQD5tr~CnI2+mWk}XP{>*xt5Rr*I= zSN>@W>$z!?n`uFWQp(5QA11WX#ofATj>4!56gC!dM}%5q#gksoWHO9LCioG4~ni^ zvaxA*XWk}m+AE63Nt844hpFm|4F$Cg&DA}d0yW<${;gCCk_G&}o&Nyd`>rPfXI8z` zB&J_C(r)A5@&H>GgGZm+9X6Y_-Bg1GlyS5S+GJ(hPF&zl;@OoMzfXQ4%H3}qu=yQISBn{C}L2@>ECfZy-8J;uXk zxhW%H8DB0#uZj8FgfHi6)ep@>EIOa+r zbvAa2>5gM8L8An=QbaF&dZ^EW1W@CF-3(A7-2yq?!OsB3Zf@h%*wKEA=oz|5S`Ea1 z?HfF*mW-o6$`!_Mtn9pxdaxW>m3iJfy3qT4DBz-BCCd=1a>ceyL^eg>+@wSO18{YB zi|k~8EpekCB{A>`0l#-P2w4tRjl|SAd7Kc*ZoE&UzHBGE%z|Y?KK2`Q*1xUaEyqE} z2qwY#7;vn2$r4v-wn;QSuuArBZ;t2}!%A)vyk746yfnvax@qo{z)uCxgGy0MZXJ1J zDnH?b7m2>RH5zP{i|px9vcL$!fo)T$({2a7etyl7P&E@Zuhf zO}9U2D28ls;j=xvF~B|!_rwe-mZT>qIwa?RR2(0Qw4VvocQfFf41c~^T<*%V4uu+S zEtJ_!ixV=^2uT?fC(pKf_=G74MDmWt{8(&kGpX# zDY{6~7*JjZr@SvMfSKlBx|ZiQgy0zUbqNr7ZQU5rhX#Ea)?-LGvYSO;qCBe0ic)gE zWY!X(b3VkndFdkKWd^xp4MV}hM>q}IN3M>^GwZp<_0}16ipE9RXNlNiSu%A7hTMG^ zU_-fNSTm&=U7R2sz-G$0;U>KRuXnjj*66#5TIGkVfu`K$ z02v}@$XxwRQ??1F&LYV?vzVSg`-~gDwP1uhJ71|L?FjFhs|0#dG5VHi{!vHXjc_Im zI9dPcEw%Bo9zeXuIf2fIDJPgPM2fYlt?q{Z%J|S$^OQsWxMOyplCo;LlyKD!tg}h2 zOy_OgxW+~-0m~MWMp|2C|IgP9Iom%x=PK^3u;^5=+NcN+T5fdL;ik(=Q z>htKlm|=7d;OKm@eh6-5lYC((8U?0flVFH|$bMHA;P3{Q0RQ*Z`qvRm(6Dqgr z0b24`(eB@xXwKS1N_a!;iUV)a#=IduT#!A9DvfynAN8B<6FsTv;3 zJ}kmqxAYZM5nFO8UcXQdZPcrdoOGWyG0M2IBN=j1 zVqCQ@H`H!*n|R|UYY3h!TN@%wwj9X9&NLc*YDw}C?Kbqn#zvin<3HU7WvW}(A8&?} z3%*skPaR#pi>JnzW!VULdv=^!D0(|3Crx|VGW!ZX#MCkdV*4Wv?J|ys;{w8j2jq$v zC?`rKPOjeKM`UhaIotx?tQF|qdH(?EX##|?y{UQcaeMcEvqhf_ghz~lg0J5jY;3=* zTLH<>v44OXup+E~fLCq;Z$+3%@~?8|cdDLN#{0B4C%>W-rhkA7#c#SV{C{*G3}fHW_=Zuq@3e3GSf;b6x$(;6Yf9g9g?~ z1l&)@&M|~Frg{A3**BoYa&2)d+93B;i8O>uUDYk%`P5Mj%Q=kn(u%#cWekJb#I#Tz z!D+kbX6D^47yQw(qvxo&{p1*j0k*O+j!d~Jny?d>28B(=%h3jWtg;VhWiYw^5zf$rX5o&W^l9XMQu3aBw1(e!J>=a^CZCo3<@*DIZYKK|!Z~cj@(H6VAc#rO%Tn0){N@ZWoH2o}^7M1c`UGJ^R%by@E>Ae*s*TBD9 z>{?nJH!))|f_ko(q&U{rz%(uB`EJ*}d#04Wc!q_|?DXl_o%Id4pPOgDgJ^1lw3*(8 z;s$CoSqFc3F+N3l%`Ym-F8hg+J$IJikPW$)e|)mYqbgvs)@HgoDq}En&xq$G5^aCf zVehmnxplV+wA05S`B>oaU6Y;u0~A|gN`rHi#AJ}lhk12V4Rc9X(av(b&v>#Eo-uqn zpoQ}vn;PHQ<+3og7;9lbN?Iu1`us)V(n#3kBv^yDTx$&X{TV~Z$*U=8MlGQ!=iHk+ z^4ZiJvHtW~gTF10R~i5I53u9p_-ajh*4&d8dZFMcEr2Lm!;PC-$-&l>Xf4R7_UClD z6rr3&@EAXbUk_g8Vs_S|+nDh-QAw$l;}}4XEHRuuY#>>jOQ1UfxTxm`Bsl7#VPPz4Hs5CLYthgXun!_c` z#lULbpyuPan>ONJ5@tV$J-+mm>RL-Tvn4|79V1E!(M|^88N}NO@!^r%pZkJm%u{23 zzM8K!pszO7nKk4A<~NqGy%vXZt}P_MUu2 ze33jk-kB}l$cN|`?g@UaNM}_il4sfVt0AsiRnWzeu2zfTMR@NpJLZY^5xlY8DZy;q zn6GY-<;{=14)=e6%)#Jhn{0&66Rrp0UbZjXeyqkr_PjlxOovVV%Mv)(CqsI6wkgh(rp@lKmg`!j4Jh;L|aQ8?qzsEk9h_ zyT2B?Q=FHis=4O(AIkIf2PI)9C8wq1LT|FC_fhU|fVzWp)m}PUaNUS=B>w=V)00RF z^2v8WckeOVdrqBlV+KabDTO~}(sLp{Xkd4wJ5|%xt0bYZdp{vhcN)3rZZT5$VZMix z+C~pim2&mNc8)vzi^%t?x$yR7^fEiU?GzE~V&vvR8r~cHv4VEf?779I_xWvueVixx zr6|+ONp0nC9z>&OvZtkBTyMe>^qFQyy*4L@yto-2^ckE9R!4b*w0h>EuPG%M^OHxN z>H6Z=XDQdiB(ES}t|fN5UxxlKP~nQ{P`Yarrnis{byld((nn_w(pO|;n{%w3bWMWu zPiC`$D=CbrF!;MO^F){S>c>B4DI$ly|Ck z{z}OJpxM_3$lt`j4YX=Xr-|B0cCxG`lw6_{0jP`23VGc9Sit>z(1ggPfpC(Ffan8;qWp z!+s5AhMuem`cVG>IQMm5;T819ST3FtlvDPD^SqXlQ_|TNjd2|157b%hcm*!FMdkRGjJQc&dA%JMfRAaNf`HD<~N_F3b$uX@f=e>Vf?f!^O? z1bEnsoA*%Jl|CI@+QRt8{c6+G3t^}~E9oNe?@10(guNyy^#1^G*Jj@WO+37^w94OS zBixJ9qYHdEA3Q!XKZ}v)v5_hbaC=Qx(s8W5{3Xsdo^r8%cx*`K%KVOo8%&YJB!8%< z4(Ufk9)m4U&k7hJ@DzJj(<{5DmX<#1{{T4dCs94{QIpkvZMn>PIO7p2w%+`8RsAx5 zcy_n{021|jm5QvFi#AKukPp)ZizMc+xmcI;`IVPT`>*?SeKL;c>AL=_{{T6C;#|i6 z0JlfTay{3xPxx)e+fFeUY2~DkpjHY1^l~;Bv@!2m|oxePB_MsD&IM z7R({+60686}ZsjF-x%i;;oMr6Z6mr{c?=6-7 zaV|28_?vS3`uWm6BHjfXao4PRKmPz2f(I?WP2#GrJn7^aMLg82@Vm)(gn7k_>W}e~ z?=xO9M*1UVWzxMy{B!)B z+`pguzYI=b+t$^<4-(|l)<%~L7;m(7Uya3%Mbj-uE*=|c`ekhpYfhT}o)}Qrel1?T z-~J{=(nLQG4{bRf=@8&TFs}%dVpy3j^6YPb0CExEei$v?+i-3F0K?qC04mC_lON{` z6}w;Zzs^qNG}80QBJ5E{R>{^neJ=9%UZrGvfJLg2Bf_0oMUPMAyq9}b{wZqmaa0WJ zhSMZ*F0#9_iLmOb<*$uos$m%e8miz|OrPFs)%gA{SCjj}C%G$Rer4IoGLtsxmlVqQ zYigN4>MEa>Evuh~CotXXZRLDkC1J4;X5E0T#g zXn1daH;mV`dq;~cPwuL`k8&^$F`F^wF3-M4yDJT}k*@=m1$Jx&2a09)*-^SR%HzWk z`#C}_YOVTFTV&EwP@|@4G?Ca)PA#{F!gX&Ary27x zh{AA7C3ow#NoPHBZdBXW+q_lQ^OI|w7=DP+?O#kmjE+*Ql}WdaBjnfk$@l)(R{sEL z$0V|rr{dn;w~HZVABx1jqCYw^sEHqh5}vE`#E*C`onPIm{{Z(muHF;Z{Y8I_He~*9 zm1pJa8{e(p`N>3-Wg70wR^1sCrZQb$skhyK{jk}R?{TBUalAe_OQx@!Q~Am+J8c(Z z`gGu>JYKzMVtky3aEN{w#wYZYIF=?Yt48@ojUR^hIX)R{Hd1d;kFNL?N%34=T<$kt zTsyLig0!|C{{ZI?h3d-Jz~E-r9kz-g(Hp;fHjLu#PFkDmob6s)e%8$8T&X;Ty2pMy zl}PYTIjO$UTYE_olJzj!aFe(;opH)Loge||QI6$#{M*8cql7ei75+0xrcvc9^t@!Z zeB{Y`vcqffua+AT{V~#caKJaR$5;0nT(#3~#K}H5wytvQ&Rv~Xqh=tID`iyk1Ng$t zG`0!uR!6%sK~=!%ww$e_);t_r&Q+1_$(I``9+_7XJdG!$co@lMP2CaoIpMKK4w(yt zA^GDN9&9=0!2^jH*PC~cv$32wUumeWLI?4A!%cAtv(EFg_&Z0hYqz99fAVtcuqR^Z z6?r_*M{G*%bZ=59zDc=*+>F2Qqi^d_eR%DaeW4y*-!|RT%PygPR=p*sC?^Ay$~^f$ z#t+>R;{B4pyI6bHCw?F(m(iB>wo>+XOLtzBHtx-d?^w%IIY9pagh~+Oy3%CmJ61d+ zs=XQq&o(pc&uUC+1`(6mHA33d@@^IZiMUgX-*z@k}2(CJH@!Z93O05 zp+^{MkSRMICkrCC?{{X&O6t2l# zrdskHI;$cz24!3z$mTOG_Q>zqLGv%wtTvVWy%YJ!w;s;WsQ&=7vV1#3f2Ie0ihdgo zd4F>$*mol<xJr-BNtYAt7vVnB(@VFxno@m~CB~(z%o$gvSueKVV@~`iZ`&?a z+3ma8MqMqU%yf@!zR>j8t=v1Z>?1xeX$@og#k^cZN z?CI*@YL{kydqwLJe~glXg}JjhKy67cgy|#?BzdR9pF!*N{M}@~;N4)}k zonPA4S@K_Okt0|aD2~_0w#o#jd0c%pZSO;c)SBEH@!|E!$}kyUbcVbAJ$`ve$abw? z*~bme$#(lm>GE}H-#67Gm@n~)0N%`XdR63m%j@zj%lo*`lBIJi0_Mk}o}qYmv{&O4 z^cD7}A>TmfRq7HxrV<{^pDk7L6W;QcWO~Sn`;zMo$Um5U%fF^}e|X1F32D;xW?6%M zn=roikmlKD1jvQ|a*7_Ea4uSZOkWz79pW{{ zQ_)|l=sso$r%t}9{5iOGi?)OxnSU&;`!n;@CHpLTT|X+|4|17^LS8alc(TWkuyE+2~vCk?%iG zLh}CZGvuj+Zfrbj)GrS4+1KL~^cy`X$bI7){8eAnYCfh7_ImkhuaJ+e;9S`B2i(2z z+=I?X%;WUPed8TGC8nONi!g7qXUkP!T*!Du^q2VOhU6Y{bk|1evI#V<4R7YB@t543 zzbu2%=km^HM?ve_G-TZuXAU_o?2&jO?$L(vWzfF6U0_`M7<26lJ(j*$HwIy07F2W^ z$49o=)%~TpbqLMisa(p+SxP4n zk?RuNvZ9}gHhLEORztq|@b=Z0_hk7he1!MBue@Q>EBw|S;vc9}((l@`A9%*U7M(sU zi$10e_Imkh)AAAZybGR>2)@x2IGGQ;W2c0)*N(0jHwJu(2LAwD3!5I1 zgX7;9ho&9k4>?Osc->Y-B$dIf{M7z4@XlN`px;LK`DJ&zGxN}S%MHl&XQS5kK4uaf z@4L$lGWLb-DK%fxKU*@p%uY6)Hw&!dMLD2)?qyt({pF&*Rc4^~b>gBpM1-;$BcLOWFM(*Mwc$bFpjGe3`9uDRo1#-KO zlZVE0Y1<>+jH{s)=tpqF;wyHpx`~htGOx}~`bgM+wy%8eKf(C4TL{oMoe0 z?ZtUjw&gb;+=0~^OyhnWag10HcZx_3v6E@#c`I3JH)STeGTHaS)3Pq=dgJ2WtXgL9 z)mQ17d$EhbTJoOBpA$Gx@mTck>0fM=f;A~@tefYy`Aj60TitYOuqlWw>vaDBQKd49 zN;K`aF6pHCX7rZtEl;&%P?@KR5Cf16e2n-GUgZYz80 zjE@OHvA_+0s-){qt^HiOjM3TC7sF2B^T5;EKsG@G_rJ>Iqyn@U#-klvBN}g^Ew9R} zAH{!$PxPy+>SpnKKE0Z;^T?o1jhS2E=8=pe#FSWv=8WA{l1l zdFpM#ODW5Xy7+7V05yz88Xe=_E5c2uzDd1TRIB93rxRCa&YfHH@9yTOROIf@j!{Hc z@iqN>R&47iu4f@lKTNig+GNS`Tsx~>L^>f??UqKjuJWJN8gY)E>l!<~-d)m%+XZ0L z-Dhj#r2aBJ)I;IZM@=Od6d?XBUP@E$VNsv}2+{jEKA8{uH%r}98F+jPBd>4!{>SN3&iVze`ay0#SWx*i|L0C z2(*5ibyi02u}64CVdYhR&IuSbL`x2l7SlBC2&MN}hvqKB(y@52RXi=!{PMr-&k>J} zOI8YBoS!XQzWJnM81amTA^D>w>)$V1Cyw%$lGd-TOG^W@n{n@`^S#{flb!rSEq-&p zIFZspcJCG8Ez=uWiOo}^BTMg&@zS13XA$2QXgq(OPF zT+(SrioTXs*)IP8#l=}K1zoQ%y*YE5U#3+ZnfWx=^zinqqNG;Nq&hMwtiO--Pg~}J z{AXNC!KF@t>{eA3;}`uU7&R+DHmdzyaM+2`Bu^l$k9Pk6aW5n&a=U~%75%(2``W)o zpG=2#5gsuiUU4fS+|#F!AL-$ogPa3!7u?a1~l$4oL?!+Q;eB5?(=%N*As#?h>t{UaU$y5h=+i;x2{XP z+s9U=>e&AP8OL4ku3DcnhqgAri6o^*n62JOjG6aJmaV#KDXgDZyoEx%ETqv$`1)vuNo^C zRQ1ce+PBp-udYbd6nX{opraP&tIy_X#JFgd48HLvce{7Zk36pEbO3pPAbj%o zitnTY(+#3WjR1HDERBFx(d`KbQEfA`G-($81$!eHAiQ zU8x<0Y>y_k`C^n&xqgGoIP1Q3<9u;7NG0&^tEXYrBR5gsJtFCeSPcY9J#!@sD-Fx3;XjD9MCzgnjn8jk=0=7Cw-#h+rp z+D@wQzIiE<{{U&9Y4~L_UR8fR@)sV@vOgzk%8$y#je2A#lm}uF0Xe(;qB#|hXFaY= zVG!`_#4;fs(n#2_-@+=Tc-{-y_ERd}>Q^^=azPnv+E(}ukb3X)-!1z{Jfk7>)qhNW z35Pk!4+Jx!aiiswREL|DbMJ2n6+bK0lO@_iwHJ9RdAZsw=cwA^*Mhk^l)Gd$ld<4l z7k!Gh#sMd)Mx1V^TuRg9la0iTS_q){_0HG7{{TtR-Pc+?68`|sc9kg%N{uxi%Xu`p zsok~rp~-9!le~px;uW)wsO##o^bw=d%r+sd-%zA#t$+XhjA zUlxeM{F~K<6NP*+do;dU75B-P*><7vO8t%oNgmoWJ{?5Qdom?e8;rrb;PA)1%cRMb$kieF^Q`T@_wB!3 z*RE>&-pjP-9@|>;(_^RQjYjJz(*@$j)1~sw)3>$n>8`e2``&&|R5w{6W7~$>gHM<_ zJ>|EJGJXp_F z;Y52l@gsNrO5`p*mu(&zrZEo-zpduPU~`E4C7|AB)KU z0Dn(2ANNP&fArV;kN2ECU;2{&0J59URKNC^@Q3U&KLh^&sGlSK=f?y0f9-$rC;!=a CuB4#= literal 0 HcmV?d00001 From 6351f7c39f107173a76fa82490570bddaf24c501 Mon Sep 17 00:00:00 2001 From: Matvey6M6 Date: Sun, 10 May 2026 23:15:21 +0400 Subject: [PATCH 2/2] fix tests --- AspireApp/AspireApp.AppHost/AppHost.cs | 3 +- .../EmployeeExportIntegrationTests.cs | 87 +++++--- .../Background/SnsSqsFileExportWorker.cs | 89 ++++++-- .../FileExportReadinessProbe.cs | 197 ++++++++++++++++++ FileService/Program.cs | 16 +- README.md | 3 +- .../Messaging/SnsEmployeeEventPublisher.cs | 97 ++++++--- ServiceApi/Program.cs | 1 - image.png | Bin 49992 -> 0 bytes 9 files changed, 403 insertions(+), 90 deletions(-) create mode 100644 FileService/Infrastructure/FileExportReadinessProbe.cs delete mode 100644 image.png diff --git a/AspireApp/AspireApp.AppHost/AppHost.cs b/AspireApp/AspireApp.AppHost/AppHost.cs index 753b0e2d..df267839 100644 --- a/AspireApp/AspireApp.AppHost/AppHost.cs +++ b/AspireApp/AspireApp.AppHost/AppHost.cs @@ -7,8 +7,7 @@ .WithEnvironment("SERVICES", "s3,sns,sqs") .WithEnvironment("AWS_DEFAULT_REGION", "us-east-1") .WithEnvironment("DEBUG", "1") - .WithHttpEndpoint(port: 4566, targetPort: 4566, name: "http") - .WithExternalHttpEndpoints(); + .WithHttpEndpoint(port: 4566, targetPort: 4566, name: "http", isProxied: false); var gateway = builder.AddProject("api-gateway"); diff --git a/Backend.IntegrationTests/EmployeeExportIntegrationTests.cs b/Backend.IntegrationTests/EmployeeExportIntegrationTests.cs index 4bff6881..0b1f4e51 100644 --- a/Backend.IntegrationTests/EmployeeExportIntegrationTests.cs +++ b/Backend.IntegrationTests/EmployeeExportIntegrationTests.cs @@ -1,13 +1,14 @@ -using Aspire.Hosting; using System.Net; using System.Text.Json; +using Aspire.Hosting; +using Aspire.Hosting.Testing; namespace Backend.IntegrationTests; public sealed class EmployeeExportIntegrationTests : IAsyncLifetime { private static readonly TimeSpan StartupTimeout = TimeSpan.FromSeconds(120); - private static readonly TimeSpan ExportTimeout = TimeSpan.FromSeconds(60); + private static readonly TimeSpan ExportTimeout = TimeSpan.FromSeconds(120); private DistributedApplication? _app; @@ -17,11 +18,13 @@ public async Task InitializeAsync() _app = await builder.BuildAsync().WaitAsync(StartupTimeout); await _app.StartAsync().WaitAsync(StartupTimeout); - var apiClient = _app.CreateHttpClient("service-api-0"); var fileClient = _app.CreateHttpClient("file-service"); + var apiClient = _app.CreateHttpClient("service-api-0"); + + using var cts = new CancellationTokenSource(StartupTimeout); - await WaitUntilAvailableAsync(apiClient, "/"); - await WaitUntilAvailableAsync(fileClient, "/"); + await WaitForFileServiceReadyAsync(fileClient, cts.Token); + await WaitForApiAsync(apiClient, cts.Token); } public async Task DisposeAsync() @@ -37,14 +40,15 @@ public async Task GeneratedEmployeeIsEventuallyExportedToObjectStorage() { Assert.NotNull(_app); - var employeeId = 501; + const int employeeId = 501; + var apiClient = _app!.CreateHttpClient("service-api-0"); var fileClient = _app.CreateHttpClient("file-service"); - using var apiResponse = await apiClient.GetAsync($"/employee?id={employeeId}"); - apiResponse.EnsureSuccessStatusCode(); + using var response = await apiClient.GetAsync($"/employee?id={employeeId}"); + response.EnsureSuccessStatusCode(); - var generatedJson = await apiResponse.Content.ReadAsStringAsync(); + var generatedJson = await response.Content.ReadAsStringAsync(); var exportedJson = await WaitForExportAsync(fileClient, employeeId); Assert.Equal(Normalize(generatedJson), Normalize(exportedJson)); @@ -55,7 +59,8 @@ public async Task SameEmployeeIdReturnsCachedPayloadAndExportRemainsAvailable() { Assert.NotNull(_app); - var employeeId = 777; + const int employeeId = 777; + var apiClient = _app!.CreateHttpClient("service-api-0"); var fileClient = _app.CreateHttpClient("file-service"); @@ -68,39 +73,62 @@ public async Task SameEmployeeIdReturnsCachedPayloadAndExportRemainsAvailable() Assert.Equal(Normalize(first), Normalize(exportedJson)); } - private static async Task WaitUntilAvailableAsync(HttpClient client, string path) + private static async Task WaitForFileServiceReadyAsync(HttpClient client, CancellationToken cancellationToken) { - using var cts = new CancellationTokenSource(StartupTimeout); - - while (!cts.IsCancellationRequested) + while (!cancellationToken.IsCancellationRequested) { try { - using var response = await client.GetAsync(path, cts.Token); - if ((int)response.StatusCode < 500) + using var response = await client.GetAsync("/ready", cancellationToken); + + if (response.IsSuccessStatusCode) { return; } + + if (response.StatusCode != HttpStatusCode.ServiceUnavailable) + { + response.EnsureSuccessStatusCode(); + } } - catch (HttpRequestException) + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { + break; } - catch (TaskCanceledException) when (cts.IsCancellationRequested) + catch { - break; } + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); + } + + throw new TimeoutException("File Service не стал ready за отведённое время."); + } + + private static async Task WaitForApiAsync(HttpClient client, CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { try { - await Task.Delay(TimeSpan.FromSeconds(2), cts.Token); + using var response = await client.GetAsync("/", cancellationToken); + if (response.IsSuccessStatusCode) + { + return; + } } - catch (TaskCanceledException) when (cts.IsCancellationRequested) + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { break; } + catch + { + } + + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); } - throw new TimeoutException($"Сервис не стал доступен по пути '{path}' за отведённое время."); + throw new TimeoutException("Service API не стал доступен за отведённое время."); } private static async Task WaitForExportAsync(HttpClient fileClient, int employeeId) @@ -112,12 +140,13 @@ private static async Task WaitForExportAsync(HttpClient fileClient, int try { using var response = await fileClient.GetAsync($"/files/{employeeId}", cts.Token); + if (response.IsSuccessStatusCode) { return await response.Content.ReadAsStringAsync(cts.Token); } - if (response.StatusCode != HttpStatusCode.NotFound && (int)response.StatusCode >= 500) + if (response.StatusCode != HttpStatusCode.NotFound) { response.EnsureSuccessStatusCode(); } @@ -126,18 +155,8 @@ private static async Task WaitForExportAsync(HttpClient fileClient, int { break; } - catch (HttpRequestException) - { - } - try - { - await Task.Delay(TimeSpan.FromSeconds(2), cts.Token); - } - catch (OperationCanceledException) when (cts.IsCancellationRequested) - { - break; - } + await Task.Delay(TimeSpan.FromSeconds(2), cts.Token); } throw new TimeoutException($"Файл сотрудника {employeeId} не был выгружен в объектное хранилище за отведённое время."); diff --git a/FileService/Background/SnsSqsFileExportWorker.cs b/FileService/Background/SnsSqsFileExportWorker.cs index aa85f451..ec47296f 100644 --- a/FileService/Background/SnsSqsFileExportWorker.cs +++ b/FileService/Background/SnsSqsFileExportWorker.cs @@ -27,7 +27,9 @@ public sealed class SnsSqsFileExportWorker : BackgroundService private readonly IAmazonSimpleNotificationService _snsClient; private readonly IAmazonSQS _sqsClient; private readonly FileExportInfrastructureState _state; + private string? _queueUrl; + private string? _topicArn; public SnsSqsFileExportWorker( IOptions options, @@ -41,12 +43,14 @@ public SnsSqsFileExportWorker( _state = state; var credentials = new BasicAWSCredentials(_options.AccessKey, _options.SecretKey); + var snsConfig = new AmazonSimpleNotificationServiceConfig { ServiceURL = _options.ServiceUrl, AuthenticationRegion = _options.Region, UseHttp = _options.ServiceUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase) }; + var sqsConfig = new AmazonSQSConfig { ServiceURL = _options.ServiceUrl, @@ -60,12 +64,20 @@ public SnsSqsFileExportWorker( protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + _state.IsInitialized = false; + while (!stoppingToken.IsCancellationRequested) { try { await EnsureInfrastructureAsync(stoppingToken); _state.IsInitialized = true; + + _logger.LogInformation( + "File export infrastructure initialized. Topic={TopicArn}, Queue={QueueUrl}", + _topicArn, + _queueUrl); + break; } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) @@ -74,6 +86,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } catch (Exception ex) { + _state.IsInitialized = false; _logger.LogWarning(ex, "LocalStack infrastructure is not ready yet. Retrying initialization..."); await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken); } @@ -89,7 +102,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) MaxNumberOfMessages = 10, WaitTimeSeconds = 10, MessageAttributeNames = ["All"], - AttributeNames = ["All"] + MessageSystemAttributeNames = ["All"] }, stoppingToken); if (response.Messages.Count == 0) @@ -117,7 +130,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) private async Task EnsureInfrastructureAsync(CancellationToken cancellationToken) { - var topicArn = (await _snsClient.CreateTopicAsync(new CreateTopicRequest + _topicArn = (await _snsClient.CreateTopicAsync(new CreateTopicRequest { Name = _options.TopicName }, cancellationToken)).TopicArn; @@ -147,7 +160,7 @@ private async Task EnsureInfrastructureAsync(CancellationToken cancellationToken "Resource": "{{queueArn}}", "Condition": { "ArnEquals": { - "aws:SourceArn": "{{topicArn}}" + "aws:SourceArn": "{{_topicArn}}" } } } @@ -166,15 +179,17 @@ await _sqsClient.SetQueueAttributesAsync(new SetQueueAttributesRequest var subscriptions = await _snsClient.ListSubscriptionsByTopicAsync(new ListSubscriptionsByTopicRequest { - TopicArn = topicArn + TopicArn = _topicArn }, cancellationToken); - var exists = subscriptions.Subscriptions.Any(s => string.Equals(s.Endpoint, queueArn, StringComparison.OrdinalIgnoreCase)); - if (!exists) + var existingSubscription = subscriptions.Subscriptions + .FirstOrDefault(s => string.Equals(s.Endpoint, queueArn, StringComparison.OrdinalIgnoreCase)); + + if (existingSubscription is null) { await _snsClient.SubscribeAsync(new SubscribeRequest { - TopicArn = topicArn, + TopicArn = _topicArn, Protocol = "sqs", Endpoint = queueArn, Attributes = new Dictionary @@ -183,24 +198,38 @@ await _snsClient.SubscribeAsync(new SubscribeRequest } }, cancellationToken); } - - _logger.LogInformation( - "File export infrastructure initialized. Topic={TopicArn}, Queue={QueueUrl}", - topicArn, - _queueUrl); + else if (!string.IsNullOrWhiteSpace(existingSubscription.SubscriptionArn) && + !string.Equals(existingSubscription.SubscriptionArn, "PendingConfirmation", StringComparison.OrdinalIgnoreCase)) + { + await _snsClient.SetSubscriptionAttributesAsync(new SetSubscriptionAttributesRequest + { + SubscriptionArn = existingSubscription.SubscriptionArn, + AttributeName = "RawMessageDelivery", + AttributeValue = "true" + }, cancellationToken); + } } private async Task ProcessMessageAsync(Message message, CancellationToken cancellationToken) { - var envelope = JsonSerializer.Deserialize(message.Body, JsonOptions); - if (envelope is null || envelope.Payload.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null) + var payloadJson = ExtractPayloadJson(message.Body); + + if (string.IsNullOrWhiteSpace(payloadJson)) { _logger.LogWarning("Received empty employee export message"); return; } - var json = envelope.Payload.GetRawText(); - await _fileStorage.SaveEmployeeJsonAsync(envelope.EmployeeId, json, cancellationToken); + var envelope = JsonSerializer.Deserialize(payloadJson, JsonOptions); + if (envelope is null || envelope.Payload.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null) + { + _logger.LogWarning("Received invalid employee export message: {Body}", message.Body); + return; + } + + var employeeJson = envelope.Payload.GetRawText(); + + await _fileStorage.SaveEmployeeJsonAsync(envelope.EmployeeId, employeeJson, cancellationToken); _logger.LogInformation( "Employee {EmployeeId} exported to object storage from replica {ReplicaId}", @@ -208,6 +237,32 @@ private async Task ProcessMessageAsync(Message message, CancellationToken cancel envelope.ReplicaId); } + private static string? ExtractPayloadJson(string? body) + { + if (string.IsNullOrWhiteSpace(body)) + { + return null; + } + + try + { + using var document = JsonDocument.Parse(body); + var root = document.RootElement; + + if (root.TryGetProperty("Message", out var messageProperty) && + messageProperty.ValueKind == JsonValueKind.String) + { + return messageProperty.GetString(); + } + } + catch + { + // Если body не envelope SNS, считаем что это raw JSON. + } + + return body; + } + public override void Dispose() { _snsClient.Dispose(); @@ -225,4 +280,4 @@ private sealed class EmployeeGeneratedEnvelope public JsonElement Payload { get; init; } } -} +} \ No newline at end of file diff --git a/FileService/Infrastructure/FileExportReadinessProbe.cs b/FileService/Infrastructure/FileExportReadinessProbe.cs new file mode 100644 index 00000000..fcb78df4 --- /dev/null +++ b/FileService/Infrastructure/FileExportReadinessProbe.cs @@ -0,0 +1,197 @@ +using Amazon.Runtime; +using Amazon.SimpleNotificationService; +using Amazon.SimpleNotificationService.Model; +using Amazon.SQS; +using Amazon.SQS.Model; +using File.Service.Configuration; +using Microsoft.Extensions.Options; +using System.Reflection; + +namespace File.Service.Infrastructure; + +public sealed class FileExportReadinessProbe +{ + private readonly AwsStorageOptions _options; + private readonly ILogger _logger; + + public FileExportReadinessProbe( + IOptions options, + ILogger logger) + { + _options = options.Value; + _logger = logger; + } + + public async Task IsReadyAsync(CancellationToken cancellationToken) + { + var serviceUrl = GetStringProperty(_options, "ServiceUrl", "AwsServiceUrl"); + var region = GetStringProperty(_options, "Region", "RegionName") ?? "us-east-1"; + var accessKey = GetStringProperty(_options, "AccessKey", "AccessKeyId") ?? "test"; + var secretKey = GetStringProperty(_options, "SecretKey", "SecretAccessKey") ?? "test"; + var topicName = GetStringProperty(_options, "TopicName", "SnsTopicName"); + var queueName = GetStringProperty(_options, "QueueName", "SqsQueueName"); + + if (string.IsNullOrWhiteSpace(serviceUrl) || + string.IsNullOrWhiteSpace(topicName) || + string.IsNullOrWhiteSpace(queueName)) + { + _logger.LogDebug( + "File export readiness is false because required AWS settings are missing. ServiceUrl={ServiceUrl}, TopicName={TopicName}, QueueName={QueueName}", + serviceUrl, + topicName, + queueName); + + return false; + } + + try + { + var credentials = new BasicAWSCredentials(accessKey, secretKey); + + using var sns = new AmazonSimpleNotificationServiceClient( + credentials, + new AmazonSimpleNotificationServiceConfig + { + ServiceURL = serviceUrl, + AuthenticationRegion = region + }); + + using var sqs = new AmazonSQSClient( + credentials, + new AmazonSQSConfig + { + ServiceURL = serviceUrl, + AuthenticationRegion = region + }); + + var topicArn = await FindTopicArnAsync(sns, topicName, cancellationToken); + if (string.IsNullOrWhiteSpace(topicArn)) + { + return false; + } + + var queueUrlResponse = await sqs.GetQueueUrlAsync( + new GetQueueUrlRequest + { + QueueName = queueName + }, + cancellationToken); + + if (string.IsNullOrWhiteSpace(queueUrlResponse.QueueUrl)) + { + return false; + } + + var subscriptionExists = await HasQueueSubscriptionAsync( + sns, + topicArn, + queueName, + cancellationToken); + + return subscriptionExists; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "File export readiness probe failed."); + return false; + } + } + + private static async Task FindTopicArnAsync( + IAmazonSimpleNotificationService sns, + string topicName, + CancellationToken cancellationToken) + { + string? nextToken = null; + + do + { + var response = await sns.ListTopicsAsync( + new ListTopicsRequest + { + NextToken = nextToken + }, + cancellationToken); + + var topic = response.Topics.FirstOrDefault(t => + !string.IsNullOrWhiteSpace(t.TopicArn) && + t.TopicArn.EndsWith($":{topicName}", StringComparison.OrdinalIgnoreCase)); + + if (topic is not null) + { + return topic.TopicArn; + } + + nextToken = response.NextToken; + } + while (!string.IsNullOrWhiteSpace(nextToken)); + + return null; + } + + private static async Task HasQueueSubscriptionAsync( + IAmazonSimpleNotificationService sns, + string topicArn, + string queueName, + CancellationToken cancellationToken) + { + string? nextToken = null; + + do + { + var response = await sns.ListSubscriptionsByTopicAsync( + new ListSubscriptionsByTopicRequest + { + TopicArn = topicArn, + NextToken = nextToken + }, + cancellationToken); + + var exists = response.Subscriptions.Any(subscription => + !string.IsNullOrWhiteSpace(subscription.Endpoint) && + EndpointMatchesQueue(subscription.Endpoint, queueName)); + + if (exists) + { + return true; + } + + nextToken = response.NextToken; + } + while (!string.IsNullOrWhiteSpace(nextToken)); + + return false; + } + + private static bool EndpointMatchesQueue(string endpoint, string queueName) + { + return endpoint.EndsWith($"/{queueName}", StringComparison.OrdinalIgnoreCase) || + endpoint.Contains($":{queueName}", StringComparison.OrdinalIgnoreCase) || + endpoint.Contains(queueName, StringComparison.OrdinalIgnoreCase); + } + + private static string? GetStringProperty(object source, params string[] names) + { + var type = source.GetType(); + + foreach (var name in names) + { + var property = type.GetProperty( + name, + BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); + + if (property?.PropertyType != typeof(string)) + { + continue; + } + + var value = property.GetValue(source) as string; + if (!string.IsNullOrWhiteSpace(value)) + { + return value; + } + } + + return null; + } +} \ No newline at end of file diff --git a/FileService/Program.cs b/FileService/Program.cs index 5b34313c..a0798758 100644 --- a/FileService/Program.cs +++ b/FileService/Program.cs @@ -6,7 +6,6 @@ builder.AddServiceDefaults(); -builder.Logging.ClearProviders(); builder.Logging.AddJsonConsole(options => { options.IncludeScopes = true; @@ -14,8 +13,8 @@ }); builder.Services.Configure(builder.Configuration.GetSection(AwsStorageOptions.SectionName)); + builder.Services.AddSingleton(); -builder.Services.AddHealthChecks().AddCheck("file-export"); builder.Services.AddSingleton(); builder.Services.AddHostedService(); @@ -26,10 +25,17 @@ app.MapGet("/", () => Results.Ok(new { service = "File.Service", - description = "Файловый сервис, сохраняющий сведения о сотрудниках в S3 через LocalStack", - endpoints = new[] { "/files/1" } + description = "Файловый сервис, сохраняющий сведения о сотрудниках в объектное хранилище", + endpoints = new[] { "/ready", "/files/{id}" } })); +app.MapGet("/ready", (FileExportInfrastructureState state) => +{ + return state.IsInitialized + ? Results.Ok(new { status = "ready" }) + : Results.StatusCode(StatusCodes.Status503ServiceUnavailable); +}); + app.MapGet("/files/{id:int}", async (int id, IEmployeeFileStorage storage, CancellationToken cancellationToken) => { if (id <= 0) @@ -48,4 +54,4 @@ app.Run(); -public partial class Program; +public partial class Program; \ No newline at end of file diff --git a/README.md b/README.md index c1588610..8ea40cc3 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,6 @@ dotnet run --project ./AspireApp/AspireApp.AppHost/AspireApp.AppHost.csproj --la ```bash curl -k "https://localhost:15000/employee?id=101" ``` -![alt text](image1.png) - ### 2. Проверка gateway ```bash @@ -50,6 +48,7 @@ curl "http://localhost:16000/files/101" ```bash dotnet test ./Backend.IntegrationTests/Backend.IntegrationTests.csproj ``` +![alt text](image1.png) ## Ключевые endpoints diff --git a/ServiceApi/Messaging/SnsEmployeeEventPublisher.cs b/ServiceApi/Messaging/SnsEmployeeEventPublisher.cs index b01cc06b..9d6efa11 100644 --- a/ServiceApi/Messaging/SnsEmployeeEventPublisher.cs +++ b/ServiceApi/Messaging/SnsEmployeeEventPublisher.cs @@ -1,5 +1,4 @@ using System.Text.Json; -using Amazon; using Amazon.Runtime; using Amazon.SimpleNotificationService; using Amazon.SimpleNotificationService.Model; @@ -49,41 +48,81 @@ public async Task PublishAsync(Employee employee, CancellationToken cancellation { ArgumentNullException.ThrowIfNull(employee); - var topicArn = await EnsureTopicAsync(cancellationToken); - var message = new EmployeeGeneratedMessage + Exception? lastException = null; + + for (var attempt = 1; attempt <= 15; attempt++) { - EmployeeId = employee.Id, - PublishedAtUtc = DateTime.UtcNow, - ReplicaId = _replicaId, - Payload = employee - }; + cancellationToken.ThrowIfCancellationRequested(); - var payload = JsonSerializer.Serialize(message, JsonOptions); + try + { + var topicArn = await EnsureTopicAsync(cancellationToken); - _logger.LogInformation( - "Publishing employee {EmployeeId} to SNS topic {TopicArn}", - employee.Id, - topicArn); + var message = new EmployeeGeneratedMessage + { + EmployeeId = employee.Id, + PublishedAtUtc = DateTime.UtcNow, + ReplicaId = _replicaId, + Payload = employee + }; - await _snsClient.PublishAsync(new PublishRequest - { - TopicArn = topicArn, - Subject = $"employee-{employee.Id}", - Message = payload, - MessageAttributes = new Dictionary - { - ["employeeId"] = new() + var payload = JsonSerializer.Serialize(message, JsonOptions); + + _logger.LogInformation( + "Publishing employee {EmployeeId} to SNS topic {TopicArn}. Attempt {Attempt}", + employee.Id, + topicArn, + attempt); + + await _snsClient.PublishAsync(new PublishRequest { - DataType = "Number", - StringValue = employee.Id.ToString() - }, - ["replicaId"] = new() + TopicArn = topicArn, + Subject = $"employee-{employee.Id}", + Message = payload, + MessageAttributes = new Dictionary + { + ["employeeId"] = new() + { + DataType = "Number", + StringValue = employee.Id.ToString() + }, + ["replicaId"] = new() + { + DataType = "String", + StringValue = _replicaId + } + } + }, cancellationToken); + + return; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception ex) + { + lastException = ex; + _topicArn = null; + + _logger.LogWarning( + ex, + "Failed to publish employee {EmployeeId} to SNS on attempt {Attempt}. Retrying...", + employee.Id, + attempt); + + if (attempt == 15) { - DataType = "String", - StringValue = _replicaId + break; } + + await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken); } - }, cancellationToken); + } + + throw new InvalidOperationException( + $"Не удалось опубликовать сотрудника {employee.Id} в SNS после повторных попыток.", + lastException); } private async Task EnsureTopicAsync(CancellationToken cancellationToken) @@ -107,4 +146,4 @@ public async ValueTask DisposeAsync() _snsClient.Dispose(); await Task.CompletedTask; } -} +} \ No newline at end of file diff --git a/ServiceApi/Program.cs b/ServiceApi/Program.cs index ee3757ed..26560057 100644 --- a/ServiceApi/Program.cs +++ b/ServiceApi/Program.cs @@ -7,7 +7,6 @@ builder.AddServiceDefaults(); builder.AddRedisDistributedCache("RedisCache"); -builder.Logging.ClearProviders(); builder.Logging.AddJsonConsole(options => { options.IncludeScopes = true; diff --git a/image.png b/image.png deleted file mode 100644 index fdd299213c7283c8808e9c1bfbd4f20e76e76000..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49992 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 ALjV8(