Skip to content
Open
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
43 changes: 27 additions & 16 deletions Jobs/RemindersJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,6 @@ internal static async Task<bool> DeliverAsync(
Action<string> 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))
{
Expand All @@ -68,6 +62,16 @@ internal static async Task<bool> 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
Expand All @@ -78,20 +82,27 @@ internal static async Task<bool> 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<string> 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)
Expand Down
20 changes: 20 additions & 0 deletions Morpheus.Tests/RemindersJobTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> 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)]
Expand Down