diff --git a/Jobs/RemindersJob.cs b/Jobs/RemindersJob.cs index 3c63a7b..fa6bde7 100644 --- a/Jobs/RemindersJob.cs +++ b/Jobs/RemindersJob.cs @@ -53,12 +53,6 @@ internal static async Task DeliverAsync( Action log, DateTime now) { - if (sendAsync == null) - { - log($"Channel {reminder.ChannelId} not found, deleting reminder {reminder.Id}."); - return true; - } - if (reminder.FirstDeliveryFailureAt.HasValue && now >= reminder.FirstDeliveryFailureAt.Value.Add(MaximumRetryAge)) { @@ -68,6 +62,16 @@ internal static async Task DeliverAsync( return true; } + if (sendAsync == null) + { + ScheduleRetry( + reminder, + now, + log, + $"Channel {reminder.ChannelId} is unavailable for reminder {reminder.Id}"); + return false; + } + string content = string.IsNullOrWhiteSpace(reminder.Text) ? "Reminder!" : reminder.Text; try @@ -78,20 +82,27 @@ internal static async Task DeliverAsync( } catch (Exception ex) { - reminder.FirstDeliveryFailureAt ??= now; - reminder.DeliveryFailureCount++; - - DateTime retryDeadline = reminder.FirstDeliveryFailureAt.Value.Add(MaximumRetryAge); - DateTime nextAttempt = now.Add(CalculateRetryDelay(reminder.DeliveryFailureCount)); - reminder.NextDeliveryAttemptAt = nextAttempt < retryDeadline ? nextAttempt : retryDeadline; - - log( - $"Error sending reminder {reminder.Id} to channel {reminder.ChannelId}: {ex.Message}. " + - $"Retry {reminder.DeliveryFailureCount} scheduled for {reminder.NextDeliveryAttemptAt:u}."); + ScheduleRetry( + reminder, + now, + log, + $"Error sending reminder {reminder.Id} to channel {reminder.ChannelId}: {ex.Message}"); return false; } } + private static void ScheduleRetry(Reminder reminder, DateTime now, Action log, string failure) + { + reminder.FirstDeliveryFailureAt ??= now; + reminder.DeliveryFailureCount++; + + DateTime retryDeadline = reminder.FirstDeliveryFailureAt.Value.Add(MaximumRetryAge); + DateTime nextAttempt = now.Add(CalculateRetryDelay(reminder.DeliveryFailureCount)); + reminder.NextDeliveryAttemptAt = nextAttempt < retryDeadline ? nextAttempt : retryDeadline; + + log($"{failure}. Retry {reminder.DeliveryFailureCount} scheduled for {reminder.NextDeliveryAttemptAt:u}."); + } + internal static TimeSpan CalculateRetryDelay(int failureCount) { if (failureCount <= 1) diff --git a/Morpheus.Tests/RemindersJobTests.cs b/Morpheus.Tests/RemindersJobTests.cs index 68b05e4..495f0a9 100644 --- a/Morpheus.Tests/RemindersJobTests.cs +++ b/Morpheus.Tests/RemindersJobTests.cs @@ -45,6 +45,26 @@ public async Task DeliverAsync_WhenDeliverySucceeds_DeletesReminder() Assert.Equal(["Remember this"], sentMessages); } + [Fact] + public async Task DeliverAsync_WhenChannelUnavailable_RetainsReminderForRetry() + { + Reminder reminder = new() { Id = 7, ChannelId = 42, Text = "Remember this" }; + List logs = []; + DateTime now = new(2026, 7, 31, 8, 0, 0, DateTimeKind.Utc); + + bool shouldDelete = await RemindersJob.DeliverAsync( + reminder, + null, + logs.Add, + now); + + Assert.False(shouldDelete); + Assert.Equal(1, reminder.DeliveryFailureCount); + Assert.Equal(now, reminder.FirstDeliveryFailureAt); + Assert.Equal(now.AddMinutes(1), reminder.NextDeliveryAttemptAt); + Assert.Contains(logs, message => message.Contains("unavailable") && message.Contains("Retry 1 scheduled")); + } + [Theory] [InlineData(1, 1)] [InlineData(2, 2)]