Skip to content

Commit

Permalink
Merge pull request #23 from shishnk/health-checks-and-deploy
Browse files Browse the repository at this point in the history
Fixes, adding health checks, changing docker-compose.yml
  • Loading branch information
shishnk committed Apr 14, 2024
2 parents 203ddf5 + d04cd18 commit d2ca05a
Show file tree
Hide file tree
Showing 22 changed files with 197 additions and 100 deletions.
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

0 comments on commit d2ca05a

Please sign in to comment.