feat: keep spread warmup reply-backs and deferred warmup sends inside short warmup windows and date an overdue next-warmup label in the mailbox list

This commit is contained in:
Matthew Meszaros
2026-10-21 10:34:22 +02:00
parent 494ed2c06f
commit e62d80fa58
3 changed files with 14 additions and 6 deletions
+6 -2
View File
@@ -49,8 +49,12 @@ func (s *JobsService) scheduleWarmupReplyBack(ctx context.Context, token *models
time.Duration(uint64(binary.BigEndian.Uint32(token.Token[4:8]))%uint64(replyBackMaxDelay-replyBackMinDelay)))
// A reply at 04:00 from a mailbox that never sends then is worse than none.
if rolled := withinWarmupHours(at, recipient); !rolled.Equal(at) {
// Spread replies rolled to the opening over its first hour, so the pool does not answer on the stroke of it.
at = rolled.Add(time.Duration(binary.BigEndian.Uint32(token.Token[8:12])%3600) * time.Second)
// Spread replies rolled to the opening over its first hour (or the whole window when shorter), so the pool does not answer on the stroke of it.
spread := uint32(3600)
if span := models.ClockMinutes(recipient.WarmupEndTime, 20*60) - models.ClockMinutes(recipient.WarmupStartTime, 8*60); span > 0 && span*60 < int(spread) {
spread = uint32(span * 60)
}
at = rolled.Add(time.Duration(binary.BigEndian.Uint32(token.Token[8:12])%spread) * time.Second)
}
moved, err := lineage.BindWarmupSuccessor(ctx, token.TaskID, recipientAccountID, receivedIDs[0], at)
+5 -2
View File
@@ -245,8 +245,11 @@ func (s *tasksService) HandleEmailTask(task *proto.ProcessTask) *errx.Error {
return errx.InternalError()
}
if at.After(time.Now()) {
// The gate answers with the window's opening; spread deferred sends past it rather than firing on the stroke.
at = at.Add(time.Duration(1+rand.Intn(20*60)) * time.Second)
// The gate answers with the window's opening; spread deferred sends past it, unless that would leave a short window.
jittered := at.Add(time.Duration(1+rand.Intn(20*60)) * time.Second)
if again, gerr := gate.WarmupDispatchNotBefore(ctx, account.ID, jittered); gerr == nil && !again.After(jittered) {
at = jittered
}
if err := lineage.RescheduleWarmupTask(ctx, taskID, at); err != nil {
return errx.InternalError()
}
+3 -2
View File
@@ -1,10 +1,11 @@
// A queued send time as a short label: "10:08" today, "tomorrow 09:12", or "Mon 09:12" later in the week.
// A queued send time as a short label: "10:08" today, "tomorrow 09:12", "Mon 09:12" later in the week, and a dated label otherwise (an overdue slot included).
export function nextSlotLabel(at: Date | string, now: Date = new Date()): string {
const d = new Date(at);
const time = d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
const day = (x: Date) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
const days = Math.round((day(d) - day(now)) / 86_400_000);
if (days <= 0) return time;
if (days === 0) return time;
if (days < 0) return `${d.toLocaleDateString([], { month: "short", day: "numeric" })} ${time}`;
if (days === 1) return `tomorrow ${time}`;
if (days < 7) return `${d.toLocaleDateString([], { weekday: "short" })} ${time}`;
return `${d.toLocaleDateString([], { month: "short", day: "numeric" })} ${time}`;