-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add job scheduler for DeleteTempReply (#212)
- Loading branch information
Showing
11 changed files
with
713 additions
and
108 deletions.
There are no files selected for viewing
101 changes: 101 additions & 0 deletions
101
src/DiscordTranslationBot/Commands/TempReplies/DeleteTempReply.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
using System.ComponentModel.DataAnnotations; | ||
using Discord; | ||
using Discord.Net; | ||
using DiscordTranslationBot.Discord.Models; | ||
|
||
namespace DiscordTranslationBot.Commands.TempReplies; | ||
|
||
/// <summary> | ||
/// Deletes a temp reply. | ||
/// If there is a reaction associated with the source message, it will be cleared, too. | ||
/// </summary> | ||
public sealed class DeleteTempReply : ICommand | ||
{ | ||
/// <summary> | ||
/// The temp reply to delete. | ||
/// </summary> | ||
[Required] | ||
public required IUserMessage Reply { get; init; } | ||
|
||
/// <summary> | ||
/// The source message ID that the temp reply is associated with. | ||
/// </summary> | ||
public required ulong SourceMessageId { get; init; } | ||
|
||
/// <summary> | ||
/// The reaction associated with the source message, if any. | ||
/// </summary> | ||
public ReactionInfo? ReactionInfo { get; init; } | ||
} | ||
|
||
public sealed partial class DeleteTempReplyHandler : ICommandHandler<DeleteTempReply> | ||
{ | ||
private readonly Log _log; | ||
|
||
public DeleteTempReplyHandler(ILogger<DeleteTempReplyHandler> logger) | ||
{ | ||
_log = new Log(logger); | ||
} | ||
|
||
public async ValueTask<Unit> Handle(DeleteTempReply command, CancellationToken cancellationToken) | ||
{ | ||
try | ||
{ | ||
// If there is also a reaction and the source message still exists, remove the reaction from it. | ||
if (command.ReactionInfo is not null) | ||
{ | ||
var sourceMessage = await command.Reply.Channel.GetMessageAsync( | ||
command.SourceMessageId, | ||
options: new RequestOptions { CancelToken = cancellationToken }); | ||
|
||
if (sourceMessage is not null) | ||
{ | ||
await sourceMessage.RemoveReactionAsync( | ||
command.ReactionInfo.Emote, | ||
command.ReactionInfo.UserId, | ||
new RequestOptions { CancelToken = cancellationToken }); | ||
} | ||
} | ||
|
||
// Delete the reply message. | ||
try | ||
{ | ||
await command.Reply.DeleteAsync(new RequestOptions { CancelToken = cancellationToken }); | ||
_log.DeletedTempMessage(command.Reply.Id); | ||
} | ||
catch (HttpException ex) when (ex.DiscordCode == DiscordErrorCode.UnknownMessage) | ||
{ | ||
// The message was likely already deleted. | ||
_log.TempMessageNotFound(command.Reply.Id); | ||
} | ||
} | ||
catch (Exception ex) | ||
{ | ||
_log.FailedToDeleteTempMessage(ex, command.Reply.Id); | ||
throw; | ||
} | ||
|
||
return Unit.Value; | ||
} | ||
|
||
private sealed partial class Log | ||
{ | ||
private readonly ILogger _logger; | ||
|
||
public Log(ILogger logger) | ||
{ | ||
_logger = logger; | ||
} | ||
|
||
[LoggerMessage(Level = LogLevel.Information, Message = "Deleted temp message ID {replyId}.")] | ||
public partial void DeletedTempMessage(ulong replyId); | ||
|
||
[LoggerMessage( | ||
Level = LogLevel.Information, | ||
Message = "Temp message ID {replyId} was not found and likely manually deleted.")] | ||
public partial void TempMessageNotFound(ulong replyId); | ||
|
||
[LoggerMessage(Level = LogLevel.Error, Message = "Failed to delete temp message ID {replyId}.")] | ||
public partial void FailedToDeleteTempMessage(Exception ex, ulong replyId); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
namespace DiscordTranslationBot.Jobs; | ||
|
||
internal static class JobExtensions | ||
{ | ||
public static IServiceCollection AddJobs(this IServiceCollection services) | ||
{ | ||
return services.AddSingleton<IScheduler, Scheduler>().AddHostedService<SchedulerBackgroundService>(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
using System.Diagnostics.CodeAnalysis; | ||
|
||
namespace DiscordTranslationBot.Jobs; | ||
|
||
public sealed partial class Scheduler : IScheduler | ||
{ | ||
private readonly Log _log; | ||
private readonly IMediator _mediator; | ||
private readonly PriorityQueue<Func<CancellationToken, Task>, DateTimeOffset> _queue = new(); | ||
private readonly TimeProvider _timeProvider; | ||
|
||
public Scheduler(IMediator mediator, TimeProvider timeProvider, ILogger<Scheduler> logger) | ||
{ | ||
_mediator = mediator; | ||
_timeProvider = timeProvider; | ||
_log = new Log(logger); | ||
} | ||
|
||
public int Count => _queue.Count; | ||
|
||
public void Schedule(ICommand command, DateTimeOffset executeAt) | ||
{ | ||
if (executeAt <= _timeProvider.GetUtcNow()) | ||
{ | ||
throw new InvalidOperationException("Tasks can only be scheduled to execute in the future."); | ||
} | ||
|
||
_queue.Enqueue(async ct => await _mediator.Send(command, ct), executeAt); | ||
_log.ScheduledCommand(command.GetType().Name, executeAt.ToLocalTime(), _queue.Count); | ||
} | ||
|
||
public void Schedule(ICommand command, TimeSpan executionDelay) | ||
{ | ||
Schedule(command, _timeProvider.GetUtcNow() + executionDelay); | ||
} | ||
|
||
public bool TryGetNextTask([NotNullWhen(true)] out Func<CancellationToken, Task>? task) | ||
{ | ||
if (_queue.TryPeek(out _, out var executeAt) && executeAt <= _timeProvider.GetUtcNow()) | ||
{ | ||
task = _queue.Dequeue(); | ||
_log.DequeuedTask(executeAt.ToLocalTime(), _queue.Count); | ||
return true; | ||
} | ||
|
||
task = null; | ||
return false; | ||
} | ||
|
||
private sealed partial class Log | ||
{ | ||
private readonly ILogger _logger; | ||
|
||
public Log(ILogger logger) | ||
{ | ||
_logger = logger; | ||
} | ||
|
||
[LoggerMessage( | ||
Level = LogLevel.Information, | ||
Message = | ||
"Scheduled command '{commandName}' to be executed at {executeAt}. Total tasks in queue: {totalTasks}.")] | ||
public partial void ScheduledCommand(string commandName, DateTimeOffset executeAt, int totalTasks); | ||
|
||
[LoggerMessage( | ||
Level = LogLevel.Information, | ||
Message = | ||
"Dequeued a task scheduled to be executed at {executeAt}. Remaining tasks in queue: {remainingTasks}.")] | ||
public partial void DequeuedTask(DateTimeOffset executeAt, int remainingTasks); | ||
} | ||
} | ||
|
||
public interface IScheduler | ||
{ | ||
/// <summary> | ||
/// The count tasks in the queue. | ||
/// </summary> | ||
public int Count { get; } | ||
|
||
/// <summary> | ||
/// Queues a Mediator command to run at a specific time. | ||
/// </summary> | ||
/// <param name="command">Mediator command to schedule.</param> | ||
/// <param name="executeAt">Time to execute the task at.</param> | ||
public void Schedule(ICommand command, DateTimeOffset executeAt); | ||
|
||
/// <summary> | ||
/// Queues a Mediator command to run at a specific time. | ||
/// </summary> | ||
/// <param name="command">Mediator command to schedule.</param> | ||
/// <param name="executionDelay">Delay for executing the task from now.</param> | ||
public void Schedule(ICommand command, TimeSpan executionDelay); | ||
|
||
/// <summary> | ||
/// Try to get the next scheduled task to be executed. | ||
/// If a task exists, it is dequeued. | ||
/// </summary> | ||
/// <param name="task">Scheduled task.</param> | ||
/// <returns>Scheduled task to be executed or null.</returns> | ||
public bool TryGetNextTask([NotNullWhen(true)] out Func<CancellationToken, Task>? task); | ||
} |
65 changes: 65 additions & 0 deletions
65
src/DiscordTranslationBot/Jobs/SchedulerBackgroundService.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
namespace DiscordTranslationBot.Jobs; | ||
|
||
public sealed partial class SchedulerBackgroundService : BackgroundService | ||
{ | ||
private readonly Log _log; | ||
private readonly IScheduler _scheduler; | ||
|
||
public SchedulerBackgroundService(IScheduler scheduler, ILogger<SchedulerBackgroundService> logger) | ||
{ | ||
_scheduler = scheduler; | ||
_log = new Log(logger); | ||
} | ||
|
||
public override Task StartAsync(CancellationToken cancellationToken) | ||
{ | ||
_log.Starting(); | ||
return base.StartAsync(cancellationToken); | ||
} | ||
|
||
public override Task StopAsync(CancellationToken cancellationToken) | ||
{ | ||
_log.Stopping(_scheduler.Count); | ||
return base.StopAsync(cancellationToken); | ||
} | ||
|
||
protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||
{ | ||
while (!stoppingToken.IsCancellationRequested) | ||
{ | ||
if (_scheduler.TryGetNextTask(out var task)) | ||
{ | ||
_log.TaskExecuting(); | ||
await task(stoppingToken); | ||
_log.TaskExecuted(); | ||
} | ||
|
||
// Wait some time before checking the queue again to reduce overloading CPU resources. | ||
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken); | ||
} | ||
} | ||
|
||
private sealed partial class Log | ||
{ | ||
private readonly ILogger _logger; | ||
|
||
public Log(ILogger logger) | ||
{ | ||
_logger = logger; | ||
} | ||
|
||
[LoggerMessage(Level = LogLevel.Information, Message = "Starting scheduler background service...")] | ||
public partial void Starting(); | ||
|
||
[LoggerMessage( | ||
Level = LogLevel.Information, | ||
Message = "Stopping scheduler background service with {remainingTasks} remaining tasks in the queue...")] | ||
public partial void Stopping(int remainingTasks); | ||
|
||
[LoggerMessage(Level = LogLevel.Information, Message = "Executing scheduled task...")] | ||
public partial void TaskExecuting(); | ||
|
||
[LoggerMessage(Level = LogLevel.Information, Message = "Executed scheduled task.")] | ||
public partial void TaskExecuted(); | ||
} | ||
} |
Oops, something went wrong.