From a8b2e11dd1fa76d8a0f1f6d61a87b023746fca31 Mon Sep 17 00:00:00 2001 From: ImoutoChan Date: Tue, 30 Apr 2024 06:11:37 +0500 Subject: [PATCH] Add offloader and LikesCounterUpdater --- .../UserCommands/LikeCallbackHandler.cs | 54 +++++------------- Source/WetPicsRebirth/Program.cs | 2 - .../ILikesCounterUpdater.cs | 8 +++ .../LikesCounterUpdater.cs | 49 +++++++++++++++++ ...daterOffloadServiceCollectionExtensions.cs | 24 ++++++++ .../Services/Offload/IOffloadReader.cs | 10 ++++ .../Services/Offload/IOffloader.cs | 6 ++ .../Services/Offload/OffloadHostedService.cs | 55 +++++++++++++++++++ .../Services/Offload/OffloadOptions.cs | 8 +++ .../OffloadServiceCollectionExtensions.cs | 17 ++++++ .../Services/Offload/Offloader.cs | 17 ++++++ .../ILikesToFavoritesTranslatorScheduler.cs | 11 ---- ...ritesOffloadServiceCollectionExtensions.cs | 24 ++++++++ ...LikesToFavoritesTranslatorHostedService.cs | 55 ------------------- .../LikesToFavoritesTranslatorScheduler.cs | 15 ----- Source/WetPicsRebirth/Startup.cs | 7 ++- 16 files changed, 235 insertions(+), 127 deletions(-) create mode 100644 Source/WetPicsRebirth/Services/LikesCounterUpdater/ILikesCounterUpdater.cs create mode 100644 Source/WetPicsRebirth/Services/LikesCounterUpdater/LikesCounterUpdater.cs create mode 100644 Source/WetPicsRebirth/Services/LikesCounterUpdater/LikesCounterUpdaterOffloadServiceCollectionExtensions.cs create mode 100644 Source/WetPicsRebirth/Services/Offload/IOffloadReader.cs create mode 100644 Source/WetPicsRebirth/Services/Offload/IOffloader.cs create mode 100644 Source/WetPicsRebirth/Services/Offload/OffloadHostedService.cs create mode 100644 Source/WetPicsRebirth/Services/Offload/OffloadOptions.cs create mode 100644 Source/WetPicsRebirth/Services/Offload/OffloadServiceCollectionExtensions.cs create mode 100644 Source/WetPicsRebirth/Services/Offload/Offloader.cs delete mode 100644 Source/WetPicsRebirth/Services/UserAccounts/ILikesToFavoritesTranslatorScheduler.cs create mode 100644 Source/WetPicsRebirth/Services/UserAccounts/LikesToFavoritesOffloadServiceCollectionExtensions.cs delete mode 100644 Source/WetPicsRebirth/Services/UserAccounts/LikesToFavoritesTranslatorHostedService.cs delete mode 100644 Source/WetPicsRebirth/Services/UserAccounts/LikesToFavoritesTranslatorScheduler.cs diff --git a/Source/WetPicsRebirth/Commands/UserCommands/LikeCallbackHandler.cs b/Source/WetPicsRebirth/Commands/UserCommands/LikeCallbackHandler.cs index 534dcd8..6bef674 100644 --- a/Source/WetPicsRebirth/Commands/UserCommands/LikeCallbackHandler.cs +++ b/Source/WetPicsRebirth/Commands/UserCommands/LikeCallbackHandler.cs @@ -1,40 +1,38 @@ -using System.Text.RegularExpressions; -using Telegram.Bot.Exceptions; -using WetPicsRebirth.Commands.UserCommands.Abstract; +using WetPicsRebirth.Commands.UserCommands.Abstract; using WetPicsRebirth.Data.Entities; using WetPicsRebirth.Data.Repositories.Abstract; using WetPicsRebirth.EntryPoint.Service.Notifications; using WetPicsRebirth.Extensions; -using WetPicsRebirth.Services; -using WetPicsRebirth.Services.UserAccounts; +using WetPicsRebirth.Services.LikesCounterUpdater; +using WetPicsRebirth.Services.Offload; namespace WetPicsRebirth.Commands.UserCommands; -public partial class LikeCallbackHandler : ICallbackHandler +public class LikeCallbackHandler : ICallbackHandler { - [GeneratedRegex("retry after (?\\d+)")] - private static partial Regex RetryAfterRegex(); - private const string LikeData = "vote_l"; private readonly ITelegramBotClient _telegramBotClient; private readonly IUsersRepository _usersRepository; private readonly IVotesRepository _votesRepository; - private readonly ILikesToFavoritesTranslatorScheduler _likesToFavoritesTranslatorScheduler; + private readonly IOffloader _likesToFavorites; + private readonly IOffloader _likesCounterUpdater; private readonly ILogger _logger; public LikeCallbackHandler( IUsersRepository usersRepository, IVotesRepository votesRepository, ITelegramBotClient telegramBotClient, - ILikesToFavoritesTranslatorScheduler likesToFavoritesTranslatorScheduler, - ILogger logger) + IOffloader likesToFavorites, + ILogger logger, + IOffloader likesCounterUpdater) { _usersRepository = usersRepository; _votesRepository = votesRepository; _telegramBotClient = telegramBotClient; - _likesToFavoritesTranslatorScheduler = likesToFavoritesTranslatorScheduler; + _likesToFavorites = likesToFavorites; _logger = logger; + _likesCounterUpdater = likesCounterUpdater; } public async Task Handle(CallbackNotification notification, CancellationToken token) @@ -66,33 +64,7 @@ await _telegramBotClient.AnswerCallbackQueryAsync( if (counts <= 0) return; - await _likesToFavoritesTranslatorScheduler.Schedule(vote); - await UpdateMessageWithRetries(chatId, messageId, 0, token); - } - - private async Task UpdateMessageWithRetries( - long chatId, - int messageId, - int retryCount, - CancellationToken ct) - { - try - { - var currentCount = await _votesRepository.GetCountForPost(chatId, messageId); - await _telegramBotClient.EditMessageReplyMarkupAsync( - chatId, - messageId, - Keyboards.WithLikes(currentCount), - ct); - } - catch (ApiRequestException e) when (e.Message.Contains("retry after")) - { - if (retryCount > 2) - throw; - - var after = int.Parse(RetryAfterRegex().Match(e.Message).Groups["after"].Value); - await Task.Delay(after * 1000, ct); - await UpdateMessageWithRetries(chatId, messageId, retryCount + 1, ct); - } + await _likesToFavorites.Offload(vote); + await _likesCounterUpdater.Offload(new(chatId, messageId)); } } diff --git a/Source/WetPicsRebirth/Program.cs b/Source/WetPicsRebirth/Program.cs index 500a5d5..ab1e440 100644 --- a/Source/WetPicsRebirth/Program.cs +++ b/Source/WetPicsRebirth/Program.cs @@ -3,7 +3,6 @@ using WetPicsRebirth.Data; using WetPicsRebirth.Extensions; using WetPicsRebirth.Services; -using WetPicsRebirth.Services.UserAccounts; namespace WetPicsRebirth; @@ -36,7 +35,6 @@ private static IHostBuilder CreateHostBuilder(string[] args) => .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()) .ConfigureSerilog() .ConfigureServices(x => x.AddHostedService()) - .ConfigureServices(x => x.AddHostedService()) .ConfigureServices(x => x.AddQuartzHostedService()); diff --git a/Source/WetPicsRebirth/Services/LikesCounterUpdater/ILikesCounterUpdater.cs b/Source/WetPicsRebirth/Services/LikesCounterUpdater/ILikesCounterUpdater.cs new file mode 100644 index 0000000..a1e9bdf --- /dev/null +++ b/Source/WetPicsRebirth/Services/LikesCounterUpdater/ILikesCounterUpdater.cs @@ -0,0 +1,8 @@ +namespace WetPicsRebirth.Services.LikesCounterUpdater; + +public interface ILikesCounterUpdater +{ + Task Update(MessageToUpdateCounter message); +} + +public record MessageToUpdateCounter(long ChatId, int MessageId); diff --git a/Source/WetPicsRebirth/Services/LikesCounterUpdater/LikesCounterUpdater.cs b/Source/WetPicsRebirth/Services/LikesCounterUpdater/LikesCounterUpdater.cs new file mode 100644 index 0000000..be33171 --- /dev/null +++ b/Source/WetPicsRebirth/Services/LikesCounterUpdater/LikesCounterUpdater.cs @@ -0,0 +1,49 @@ +using System.Text.RegularExpressions; +using Telegram.Bot.Exceptions; +using WetPicsRebirth.Data.Repositories.Abstract; + +namespace WetPicsRebirth.Services.LikesCounterUpdater; + +internal partial class LikesCounterUpdater : ILikesCounterUpdater +{ + [GeneratedRegex("retry after (?\\d+)")] + private static partial Regex RetryAfterRegex(); + + private readonly ITelegramBotClient _telegramBotClient; + private readonly IVotesRepository _votesRepository; + + public LikesCounterUpdater(ITelegramBotClient telegramBotClient, IVotesRepository votesRepository) + { + _telegramBotClient = telegramBotClient; + _votesRepository = votesRepository; + } + + public async Task Update(MessageToUpdateCounter message) + => await UpdateMessageWithRetries(message.ChatId, message.MessageId, 0); + + private async Task UpdateMessageWithRetries( + long chatId, + int messageId, + int retryCount, + CancellationToken ct = default) + { + try + { + var currentCount = await _votesRepository.GetCountForPost(chatId, messageId); + await _telegramBotClient.EditMessageReplyMarkupAsync( + chatId, + messageId, + Keyboards.WithLikes(currentCount), + ct); + } + catch (ApiRequestException e) when (e.Message.Contains("retry after")) + { + if (retryCount > 2) + throw; + + var after = int.Parse(RetryAfterRegex().Match(e.Message).Groups["after"].Value); + await Task.Delay(after * 1000, ct); + await UpdateMessageWithRetries(chatId, messageId, retryCount + 1, ct); + } + } +} diff --git a/Source/WetPicsRebirth/Services/LikesCounterUpdater/LikesCounterUpdaterOffloadServiceCollectionExtensions.cs b/Source/WetPicsRebirth/Services/LikesCounterUpdater/LikesCounterUpdaterOffloadServiceCollectionExtensions.cs new file mode 100644 index 0000000..1df1f6a --- /dev/null +++ b/Source/WetPicsRebirth/Services/LikesCounterUpdater/LikesCounterUpdaterOffloadServiceCollectionExtensions.cs @@ -0,0 +1,24 @@ +using WetPicsRebirth.Services.Offload; + +namespace WetPicsRebirth.Services.LikesCounterUpdater; + +public static class LikesCounterUpdaterOffloadServiceCollectionExtensions +{ + public static IServiceCollection AddLikesCounterUpdaterOffload(this IServiceCollection services) + { + services.AddTransient(); + services.AddOffload(options => + { + options.ItemProcessor + = (x, message) => x.GetRequiredService().Update(message); + + options.ErrorLogger = (logger, message, exception) => + logger.LogError( + exception, + "Unable to update likes counter for chat {ChatId} message {MessageId}", + message.ChatId, + message.MessageId); + }); + return services; + } +} diff --git a/Source/WetPicsRebirth/Services/Offload/IOffloadReader.cs b/Source/WetPicsRebirth/Services/Offload/IOffloadReader.cs new file mode 100644 index 0000000..075ba51 --- /dev/null +++ b/Source/WetPicsRebirth/Services/Offload/IOffloadReader.cs @@ -0,0 +1,10 @@ +using System.Threading.Channels; + +namespace WetPicsRebirth.Services.Offload; + +internal interface IOffloadReader +{ + ChannelReader Reader { get; } + + void Complete(); +} diff --git a/Source/WetPicsRebirth/Services/Offload/IOffloader.cs b/Source/WetPicsRebirth/Services/Offload/IOffloader.cs new file mode 100644 index 0000000..6a25ce0 --- /dev/null +++ b/Source/WetPicsRebirth/Services/Offload/IOffloader.cs @@ -0,0 +1,6 @@ +namespace WetPicsRebirth.Services.Offload; + +public interface IOffloader +{ + Task Offload(T vote); +} diff --git a/Source/WetPicsRebirth/Services/Offload/OffloadHostedService.cs b/Source/WetPicsRebirth/Services/Offload/OffloadHostedService.cs new file mode 100644 index 0000000..b3e25d1 --- /dev/null +++ b/Source/WetPicsRebirth/Services/Offload/OffloadHostedService.cs @@ -0,0 +1,55 @@ +using Microsoft.Extensions.Options; + +namespace WetPicsRebirth.Services.Offload; + +internal class OffloadHostedService : IHostedService +{ + private readonly IOffloadReader _offload; + private readonly IServiceProvider _serviceProvider; + private readonly IOptions> _options; + private readonly ILogger> _logger; + + protected OffloadHostedService( + IOffloadReader offload, + IServiceProvider serviceProvider, + IOptions> options, + ILogger> logger) + { + _offload = offload; + _serviceProvider = serviceProvider; + _options = options; + _logger = logger; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + Task.Run(Process, cancellationToken); + + return Task.CompletedTask; + } + + private async Task Process() + { + var reader = _offload.Reader; + + while (await reader.WaitToReadAsync()) + while (reader.TryRead(out var item)) + { + try + { + using var scope = _serviceProvider.CreateScope(); + await _options.Value.ItemProcessor(scope.ServiceProvider, item); + } + catch (Exception e) + { + _options.Value.ErrorLogger(_logger, item, e); + } + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _offload.Complete(); + return Task.CompletedTask; + } +} diff --git a/Source/WetPicsRebirth/Services/Offload/OffloadOptions.cs b/Source/WetPicsRebirth/Services/Offload/OffloadOptions.cs new file mode 100644 index 0000000..4defc04 --- /dev/null +++ b/Source/WetPicsRebirth/Services/Offload/OffloadOptions.cs @@ -0,0 +1,8 @@ +namespace WetPicsRebirth.Services.Offload; + +public class OffloadOptions +{ + public required Func ItemProcessor { get; set; } + + public required Action ErrorLogger { get; set; } +} diff --git a/Source/WetPicsRebirth/Services/Offload/OffloadServiceCollectionExtensions.cs b/Source/WetPicsRebirth/Services/Offload/OffloadServiceCollectionExtensions.cs new file mode 100644 index 0000000..15794e1 --- /dev/null +++ b/Source/WetPicsRebirth/Services/Offload/OffloadServiceCollectionExtensions.cs @@ -0,0 +1,17 @@ +namespace WetPicsRebirth.Services.Offload; + +public static class OffloadServiceCollectionExtensions +{ + public static IServiceCollection AddOffload( + this IServiceCollection services, Action> configure) + { + services.Configure(configure); + + services.AddSingleton>(); + services.AddTransient>(x => x.GetRequiredService>()); + services.AddTransient>(x => x.GetRequiredService>()); + services.AddHostedService>(); + + return services; + } +} diff --git a/Source/WetPicsRebirth/Services/Offload/Offloader.cs b/Source/WetPicsRebirth/Services/Offload/Offloader.cs new file mode 100644 index 0000000..5e415c4 --- /dev/null +++ b/Source/WetPicsRebirth/Services/Offload/Offloader.cs @@ -0,0 +1,17 @@ +using System.Threading.Channels; + +namespace WetPicsRebirth.Services.Offload; + +/// +/// Should be registered as a singleton. +/// > +internal class Offloader : IOffloader, IOffloadReader +{ + private Channel VotesToTranslate { get; } = Channel.CreateUnbounded(); + + public async Task Offload(T vote) => await VotesToTranslate.Writer.WriteAsync(vote); + + public ChannelReader Reader => VotesToTranslate.Reader; + + public void Complete() => VotesToTranslate.Writer.Complete(); +} diff --git a/Source/WetPicsRebirth/Services/UserAccounts/ILikesToFavoritesTranslatorScheduler.cs b/Source/WetPicsRebirth/Services/UserAccounts/ILikesToFavoritesTranslatorScheduler.cs deleted file mode 100644 index 1f0ee43..0000000 --- a/Source/WetPicsRebirth/Services/UserAccounts/ILikesToFavoritesTranslatorScheduler.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Threading.Channels; -using WetPicsRebirth.Data.Entities; - -namespace WetPicsRebirth.Services.UserAccounts; - -public interface ILikesToFavoritesTranslatorScheduler -{ - Task Schedule(Vote vote); - - Channel VotesToTranslate { get; } -} \ No newline at end of file diff --git a/Source/WetPicsRebirth/Services/UserAccounts/LikesToFavoritesOffloadServiceCollectionExtensions.cs b/Source/WetPicsRebirth/Services/UserAccounts/LikesToFavoritesOffloadServiceCollectionExtensions.cs new file mode 100644 index 0000000..8705964 --- /dev/null +++ b/Source/WetPicsRebirth/Services/UserAccounts/LikesToFavoritesOffloadServiceCollectionExtensions.cs @@ -0,0 +1,24 @@ +using WetPicsRebirth.Data.Entities; +using WetPicsRebirth.Services.Offload; + +namespace WetPicsRebirth.Services.UserAccounts; + +public static class LikesToFavoritesOffloadServiceCollectionExtensions +{ + public static IServiceCollection AddLikesToFavoritesOffload(this IServiceCollection services) + { + services.AddTransient(); + services.AddOffload(options => + { + options.ItemProcessor = (x, vote) => x.GetRequiredService().Translate(vote); + + options.ErrorLogger = (logger, vote, exception) => + logger.LogError( + exception, + "Unable to fav post chat {ChatId} message {MessageId}", + vote.ChatId, + vote.MessageId); + }); + return services; + } +} diff --git a/Source/WetPicsRebirth/Services/UserAccounts/LikesToFavoritesTranslatorHostedService.cs b/Source/WetPicsRebirth/Services/UserAccounts/LikesToFavoritesTranslatorHostedService.cs deleted file mode 100644 index e4452c8..0000000 --- a/Source/WetPicsRebirth/Services/UserAccounts/LikesToFavoritesTranslatorHostedService.cs +++ /dev/null @@ -1,55 +0,0 @@ -namespace WetPicsRebirth.Services.UserAccounts; - -public class LikesToFavoritesTranslatorHostedService : IHostedService -{ - private readonly ILikesToFavoritesTranslatorScheduler _scheduler; - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - - public LikesToFavoritesTranslatorHostedService( - ILikesToFavoritesTranslatorScheduler scheduler, - IServiceProvider serviceProvider, - ILogger logger) - { - _scheduler = scheduler; - _serviceProvider = serviceProvider; - _logger = logger; - } - - public Task StartAsync(CancellationToken cancellationToken) - { - Task.Run(Process, cancellationToken); - - return Task.CompletedTask; - } - - private async Task Process() - { - var reader = _scheduler.VotesToTranslate.Reader; - - while (await reader.WaitToReadAsync()) - while (reader.TryRead(out var item)) - { - try - { - using var scope = _serviceProvider.CreateScope(); - var translator = scope.ServiceProvider.GetRequiredService(); - await translator.Translate(item); - } - catch (Exception e) - { - _logger.LogError( - e, - "Unable to fav post chat {ChatId} message {MessageId}", - item.ChatId, - item.MessageId); - } - } - } - - public Task StopAsync(CancellationToken cancellationToken) - { - _scheduler.VotesToTranslate.Writer.Complete(); - return Task.CompletedTask; - } -} diff --git a/Source/WetPicsRebirth/Services/UserAccounts/LikesToFavoritesTranslatorScheduler.cs b/Source/WetPicsRebirth/Services/UserAccounts/LikesToFavoritesTranslatorScheduler.cs deleted file mode 100644 index 875b6e2..0000000 --- a/Source/WetPicsRebirth/Services/UserAccounts/LikesToFavoritesTranslatorScheduler.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Threading.Channels; -using WetPicsRebirth.Data.Entities; - -namespace WetPicsRebirth.Services.UserAccounts; - -// singleton -internal class LikesToFavoritesTranslatorScheduler : ILikesToFavoritesTranslatorScheduler -{ - public Channel VotesToTranslate { get; } = Channel.CreateUnbounded(); - - public async Task Schedule(Vote vote) - { - await VotesToTranslate.Writer.WriteAsync(vote); - } -} diff --git a/Source/WetPicsRebirth/Startup.cs b/Source/WetPicsRebirth/Startup.cs index f93b6d9..cefef15 100644 --- a/Source/WetPicsRebirth/Startup.cs +++ b/Source/WetPicsRebirth/Startup.cs @@ -18,6 +18,7 @@ using WetPicsRebirth.Infrastructure.ImageProcessing; using WetPicsRebirth.Jobs; using WetPicsRebirth.Services; +using WetPicsRebirth.Services.LikesCounterUpdater; using WetPicsRebirth.Services.UserAccounts; namespace WetPicsRebirth; @@ -65,7 +66,6 @@ public void ConfigureServices(IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -79,8 +79,9 @@ public void ConfigureServices(IServiceCollection services) services.AddTransient(); services.AddImageProcessing(); - services.AddSingleton(); - + services.AddLikesToFavoritesOffload(); + services.AddLikesCounterUpdaterOffload(); + // mediator services.AddMediatR(x => x.RegisterServicesFromAssemblyContaining()); var handlers = GetType().Assembly