Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes, adding health checks, changing docker-compose.yml #23

Merged
merged 1 commit into from
Apr 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
POSTGRES_DB=weather-database
POSTGRES_USER=user
POSTGRES_PASSWORD=pass
TELEGRAM_BOT_TOKEN=6819090366:AAEp-IrmVXY-U2Ie91lZktlkjxPG1IkJTJU
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.NpgSql" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public static IServiceCollection AddPersistence(this IServiceCollection services
services.AddScoped<IWeatherSubscriptionRepository, WeatherSubscriptionRepository>();
services.AddScoped<IUnitOfWork, UnitOfWork>();

services.AddHealthChecks()
.AddNpgSql(connectionString ?? throw new InvalidOperationException("DbConnection string is null"));

return services;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using DatabaseApp.Persistence;
using DatabaseApp.Persistence.DatabaseContext;
using DatabaseApp.WebApi.Middleware;
using HealthChecks.UI.Client;
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Any;
using Serilog;
Expand Down Expand Up @@ -63,6 +64,10 @@

app.SubscribeToEvents();
app.UseHttpsRedirection();
app.MapHealthChecks("/health", new()
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
app.UseExceptionHandler();
app.UseSerilogRequestLogging();
app.UseRouting();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@ FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /app

COPY *.sln .
COPY WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/*.csproj ./WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/
COPY WeatherBotApi.DatabaseApp/Core/DatabaseApp.Domain/*.csproj ./WeatherBotApi.DatabaseApp/Core/DatabaseApp.Domain/
COPY WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/*.csproj ./WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/
COPY WeatherBotApi.DatabaseApp/Presentation/DatabaseApp.WebApi/*.csproj ./WeatherBotApi.DatabaseApp/Presentation/DatabaseApp.WebApi/
COPY WeatherBotApi.DatabaseApp/Tests/DatabaseApp.Tests/*.csproj ./WeatherBotApi.DatabaseApp/Tests/DatabaseApp.Tests/
COPY WeatherBotApi.WeatherApp/Core/WeatherApp.Application/*.csproj ./WeatherBotApi.WeatherApp/Core/WeatherApp.Application/
COPY WeatherBotApi.WeatherApp/Core/WeatherApp.Domain/*.csproj ./WeatherBotApi.WeatherApp/Core/WeatherApp.Domain/
COPY WeatherBotApi.WeatherApp/Infrastructure/WeatherApp.Converters/*.csproj WeatherBotApi.WeatherApp/Infrastructure/WeatherApp.Converters/
Expand All @@ -16,17 +11,27 @@ COPY WeatherBotApi.WeatherApp/Tests/WeatherApp.Tests/*.csproj ./WeatherBotApi.We
COPY WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Domain/*.csproj ./WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Domain/
COPY WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/*.csproj ./WeatherBotApi.TelegramBotApp/Core/TelegramBotApp.Application/
COPY WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/*.csproj ./WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Messaging/
COPY WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Caching/*.csproj ./WeatherBotApi.TelegramBotApp/Infrastructure/TelegramBotApp.Caching/
COPY WeatherBotApi.TelegramBotApp/Presentation/TelegramBotApp.Api/*.csproj ./WeatherBotApi.TelegramBotApp/Presentation/TelegramBotApp.Api/
COPY WeatherBotApi.DatabaseApp/Core/DatabaseApp.Domain/*.csproj ./WeatherBotApi.DatabaseApp/Core/DatabaseApp.Domain/
COPY WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/*.csproj ./WeatherBotApi.DatabaseApp/Core/DatabaseApp.Application/
COPY WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/*.csproj ./WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.Persistence/
COPY WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/*.csproj ./WeatherBotApi.DatabaseApp/Infrastructure/DatabaseApp.IntegrationEvents/
COPY WeatherBotApi.DatabaseApp/Presentation/DatabaseApp.WebApi/*.csproj ./WeatherBotApi.DatabaseApp/Presentation/DatabaseApp.WebApi/
COPY WeatherBotApi.DatabaseApp/Tests/DatabaseApp.Tests/*.csproj ./WeatherBotApi.DatabaseApp/Tests/DatabaseApp.Tests/

RUN dotnet restore

COPY WeatherBotApi.TelegramBotApp/ ./WeatherBotApi.TelegramBotApp/
COPY WeatherBotApi.WeatherApp/ ./WeatherBotApi.WeatherApp/
COPY WeatherBotApi.DatabaseApp/ ./WeatherBotApi.DatabaseApp/

WORKDIR /app/WeatherBotApi.DatabaseApp/Presentation/DatabaseApp.WebApi/
RUN dotnet publish -c Release -o out

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
RUN apt-get update && apt-get install -y curl
COPY --from=build /app/WeatherBotApi.DatabaseApp/Presentation/DatabaseApp.WebApi/out ./

ENTRYPOINT ["dotnet", "DatabaseApp.WebApi.dll"]
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@
},
"RabbitMqSettings": {
"HostName": "localhost",
"Port": 15672,
"Username": "guest",
"Password": "guest",
"Port": 5672,
"EventQueueName": "database-event-queue",
"ResponseQueueName": "database-response-queue",
"EventExchangeName": "event-exchange",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@
},
"RabbitMqSettings": {
"HostName": "localhost",
"Port": 15672,
"Username": "guest",
"Password": "guest",
"Port": 5672,
"EventQueueName": "database-event-queue",
"ResponseQueueName": "database-response-queue",
"EventExchangeName": "event-exchange",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ public static class DependencyInjection
{
public static IServiceCollection AddApplication(this IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<ITelegramBotSettings, TelegramBotSettings>();
// services.AddSingleton<ITelegramBotSettings, TelegramBotSettings>();
services.AddSingleton<TelegramCommandValidatorFactory>();
services.AddSingleton<TelegramCommandFactory>();
// services.AddSingleton<TelegramCommandFactory>();
services.AddSingleton<ITelegramBotInitializer, TelegramBotInitializer>();
services.AddSingleton<IResendMessageService, ResendMessageService>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using Telegram.Bot.Polling;
using Telegram.Bot.Types;
using TelegramBotApp.Application.Factories;
using TelegramBotApp.Application.Services;
using TelegramBotApp.Caching.Caching;
using TelegramBotApp.Domain.Models;
using TelegramBotApp.Messaging;
Expand All @@ -25,7 +24,7 @@ public void StartReceiving(IEventBus bus, ICacheService cacheService, IResendMes
{
_telegramCommandFactory = new(bus, cacheService, messageService, _settings);
_cacheService = cacheService;

telegramBot.StartReceiving(
updateHandler: async (botClient, update, token) =>
await HandleUpdateInnerAsync(botClient, update, bus, token),
Expand All @@ -39,47 +38,48 @@ public Task SendMessageAsync(long telegramId, string message) =>

public Task<User> GetMeAsync() => telegramBot.GetMeAsync();

private async Task HandleUpdateInnerAsync(ITelegramBotClient botClient, Update update, IEventBus bus,
CancellationToken cancellationToken)
private Task HandleUpdateInnerAsync(ITelegramBotClient botClient, Update update, IEventBus bus, CancellationToken token)
{
if (update.Message is not { } message) return;
if (message.Text is not { } messageText) return;
if (update.Message is not { } message) return Task.CompletedTask;
if (message.Text is not { } messageText) return Task.CompletedTask;

var chatId = message.Chat.Id; // chat id equals to user telegram id
using var cts = new CancellationTokenSource(_settings.Timeout);

try
Task.Run(async () =>
{
_ = Task.Run(async () => await UpdateUsersCacheAsync(
message,
bus,
cancellationToken),
cancellationToken);
var result = await _telegramCommandFactory.StartCommand(messageText, chatId);

if (result.IsFailed)
using var cts = new CancellationTokenSource(_settings.Timeout);

try
{
await HandleError(botClient, chatId, result);
return;
}
await UpdateUsersCacheAsync(message, bus, cts.Token);
var result = await _telegramCommandFactory.StartCommand(messageText, chatId);

await botClient.SendTextMessageAsync(chatId: chatId, result.Value,
cancellationToken: cancellationToken);
}
catch (Exception)
{
await HandleError(botClient, chatId, result: Result.Fail("Internal error"));
}
if (result.IsFailed)
{
await HandleError(botClient, chatId, result);
return;
}

return;
await botClient.SendTextMessageAsync(chatId: chatId, result.Value,
cancellationToken: cts.Token);
}
catch (TaskCanceledException)
{
await HandleError(botClient, chatId, Result.Fail("Timeout"));
}
catch (Exception)
{
await HandleError(botClient, chatId, result: Result.Fail("Internal error"));
}
}, token);
return Task.CompletedTask;
}

async Task HandleError(ITelegramBotClient bot, long chatIdInner, IResultBase result)
{
await bot.SendTextMessageAsync(chatId: chatIdInner, result.Errors.First().Message,
cancellationToken: cancellationToken);
var text = await _telegramCommandFactory.StartCommand(HelpCommand, chatIdInner);
await bot.SendTextMessageAsync(chatId: chatIdInner, text.Value, cancellationToken: cancellationToken);
}
private async Task HandleError(ITelegramBotClient bot, long chatIdInner, IResultBase result)
{
await bot.SendTextMessageAsync(chatId: chatIdInner, result.Errors.First().Message);
var text = await _telegramCommandFactory.StartCommand(HelpCommand, chatIdInner);
await bot.SendTextMessageAsync(chatId: chatIdInner, text.Value);
}

private static Task HandlePollingErrorInner(ITelegramBotClient botClientInner, Exception exception,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ public class TelegramBotSettings : ITelegramBotSettings
public CancellationToken Token => _cts.Token;

private TelegramBotSettings() => _cts = new(Timeout);

public static ITelegramBotSettings CreateDefault() => new TelegramBotSettings();
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Composition;
using System.Globalization;
using System.Text;
using FluentResults;
using Telegram.Bot.Types;
using TelegramBotApp.Application.Factories;
using TelegramBotApp.Application.Services;
using TelegramBotApp.Caching.Caching;
using TelegramBotApp.Domain.Models;
using TelegramBotApp.Messaging;
Expand Down Expand Up @@ -66,9 +63,11 @@ public async Task<Result<string>> Execute(InvocationContext context,
IEventBus bus, ICacheService cacheService, IResendMessageService messageService,
CancellationToken cancellationToken)
{
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));

var city = context.ParseResult.GetValueForArgument(getArgument("city"));
var response = await bus.Publish(new WeatherForecastRequestIntegrationEvent(city), nameof(UniversalResponse),
CancellationToken.None); // TODO: fix cancellation token
cts.Token); // TODO: fix cancellation token

return !response.IsEmpty
? Result.Ok(response.Message)
Expand All @@ -78,11 +77,11 @@ public async Task<Result<string>> Execute(InvocationContext context,

[Export(typeof(ITelegramCommand))]
[ExportMetadata(nameof(Command), "/createSubscription")]
[ExportMetadata(nameof(Description), "<location> <resendInterval (example: 00:00:30)> - create a subscription")]
[ExportMetadata(nameof(Description), "<location> <resendInterval (example: 00:30)> - create a subscription")]
public class CreateSubscriptionTelegramCommand : ITelegramCommand
{
public string Command => "/createSubscription";
public string Description => "<location> <resendInterval (example: 00:00:30)> - create a subscription";
public string Description => "<location> <resendInterval (example: 00:30)> - create a subscription";

public IEnumerable<Argument<string>> Arguments
{
Expand All @@ -101,13 +100,25 @@ public async Task<Result<string>> Execute(InvocationContext context,
CancellationToken cancellationToken)
{
var location = context.ParseResult.GetValueForArgument(getArgument("location"));
var intervalMemory = context.ParseResult.GetValueForArgument(getArgument("resendInterval")).AsMemory();
var colonIndex = intervalMemory.Span.IndexOf(':');

// TODO: avoid code duplication, using constants
if (colonIndex == -1)
{
return "Invalid format for resend interval. Please use the format hh:mm".ToResult();
}

var hoursMemory = intervalMemory[..colonIndex];
var minutesMemory = intervalMemory[(colonIndex + 1)..];

if (!TimeSpan.TryParseExact(context.ParseResult.GetValueForArgument(getArgument("resendInterval")),
@"hh\:mm\:ss", CultureInfo.InvariantCulture, out var resendInterval))
if (!int.TryParse(hoursMemory.Span, out var hours) || !int.TryParse(minutesMemory.Span, out var minutes))
{
return "Invalid format for resend interval. Please use the format hh:mm:ss".ToResult();
return "Invalid format for resend interval. Please use the format hh:mm".ToResult();
}

var resendInterval = new TimeSpan(hours, minutes, 0);

if (resendInterval < TimeSpan.FromMinutes(30)) // hardcode
{
return "Resend interval should be at least 30 minutes".ToResult(); // TODO: add validation, result fail
Expand Down Expand Up @@ -152,12 +163,13 @@ await cacheService.GetAsync<List<UserSubscriptionInfo>>("allSubscriptions",
if (allSubscriptions == null) return Result.Fail("Bad internal state");

var message = new StringBuilder();
var userSubscriptions = allSubscriptions.Where(s => s.TelegramId == telegramId).ToList();

for (var i = 0; i < allSubscriptions.Count; i++)
for (var i = 0; i < userSubscriptions.Count; i++)
{
var subscription = allSubscriptions[i];
var subscription = userSubscriptions[i];
message.AppendLine(
$"{i + 1}) Location: {subscription.Location}, resend interval: {subscription.ResendInterval}");
$"{i + 1}) Location: {subscription.Location}, resend interval: {subscription.ResendInterval.ToString(@"hh\:mm")}");
}

return message.Length > 0
Expand All @@ -168,11 +180,11 @@ await cacheService.GetAsync<List<UserSubscriptionInfo>>("allSubscriptions",

[Export(typeof(ITelegramCommand))]
[ExportMetadata(nameof(Command), "/updateSubscription")]
[ExportMetadata(nameof(Description), "<location> <resendInterval (example: 00:00:30)> - update a subscription")]
[ExportMetadata(nameof(Description), "<location> <resendInterval (example: 00:30)> - update a subscription")]
public class UpdateSubscriptionTelegramCommand : ITelegramCommand
{
public string Command => "/updateSubscription";
public string Description => "<location> <resendInterval (example: 00:00:30)> - update a subscription";
public string Description => "<location> <resendInterval (example: 00:30)> - update a subscription";

public IEnumerable<Argument<string>> Arguments
{
Expand All @@ -190,13 +202,25 @@ public async Task<Result<string>> Execute(InvocationContext context,
CancellationToken cancellationToken)
{
var location = context.ParseResult.GetValueForArgument(getArgument("location"));
var intervalMemory = context.ParseResult.GetValueForArgument(getArgument("resendInterval")).AsMemory();
var colonIndex = intervalMemory.Span.IndexOf(':');

if (!TimeSpan.TryParseExact(context.ParseResult.GetValueForArgument(getArgument("resendInterval")),
@"hh\:mm\:ss", CultureInfo.InvariantCulture, out var resendInterval))
// TODO: avoid code duplication, using constants
if (colonIndex == -1)
{
return "Invalid format for resend interval. Please use the format hh:mm:ss".ToResult();
return "Invalid format for resend interval. Please use the format hh:mm".ToResult();
}

var hoursMemory = intervalMemory[..colonIndex];
var minutesMemory = intervalMemory[(colonIndex + 1)..];

if (!int.TryParse(hoursMemory.Span, out var hours) || !int.TryParse(minutesMemory.Span, out var minutes))
{
return "Invalid format for resend interval. Please use the format hh:mm".ToResult();
}

var resendInterval = new TimeSpan(hours, minutes, 0);

if (resendInterval < TimeSpan.FromMinutes(30)) // hardcode
{
return "Resend interval should be at least 30 minutes".ToResult(); // TODO: add validation, result fail
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ public bool TryConnect()
{
HostName = messageSettings.HostName,
Port = messageSettings.Port,
UserName = messageSettings.Username,
Password = messageSettings.Password,
DispatchConsumersAsync = true
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ public async Task Run()
{
using var host = Host.CreateDefaultBuilder()
.ConfigureAppConfiguration(config =>
config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true))
config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables())
.ConfigureServices((builder, services) => services
.AddApplication(builder.Configuration)
.AddMessaging(builder.Configuration)
Expand All @@ -47,7 +48,12 @@ public async Task Run()
var me = await botClient.GetMeAsync();

Console.WriteLine($"Start listening for @{me.Username}");
Console.ReadLine();

// strange loop for simulation ReadLine, TODO: fix
while (!cancellationTokenSource.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromMinutes(1), cancellationTokenSource.Token);
}

await cancellationTokenSource.CancelAsync();
}
Expand Down
Loading