From 4ea96115105ce4148232d1477a42e6641f70e64f Mon Sep 17 00:00:00 2001 From: Matthew Meszaros Date: Sat, 4 Jul 2026 11:36:47 +0200 Subject: [PATCH] feat: vary each mailbox's daily warmup volume by a stable per-day factor with occasional lighter days so a mailbox never sends an identical count every day --- internal/scheduler/warmup_scheduler.go | 42 ++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/internal/scheduler/warmup_scheduler.go b/internal/scheduler/warmup_scheduler.go index 44578490..06b1e74d 100644 --- a/internal/scheduler/warmup_scheduler.go +++ b/internal/scheduler/warmup_scheduler.go @@ -75,6 +75,31 @@ func warmupRampTarget(activelyWarming bool, base, increase, max, daysWarming int return target } +// dailyVolumeFactor returns a stable-per-day multiplier (0.75–1.10, with an +// occasional lighter day near 0.6) for a mailbox's warmup volume. Real senders +// don't send exactly N every day; a flat daily count is a pattern. Deterministic +// on (accountID, localDate) so the scheduler's many intra-day recomputations +// don't thrash the target. +func dailyVolumeFactor(accountID uuid.UUID, day time.Time) float64 { + y, m, d := day.Date() + var seed uint64 = 1469598103934665603 // FNV-1a offset basis + mix := func(b []byte) { + for _, c := range b { + seed ^= uint64(c) + seed *= 1099511628211 + } + } + id := accountID + mix(id[:]) + mix([]byte{byte(y), byte(y >> 8), byte(m), byte(d)}) + + u := float64(seed%1000) / 1000.0 // stable [0,1) + if u < 0.15 { + return 0.60 // ~15% of days are lighter + } + return 0.75 + u*0.35 // 0.75–1.10 +} + // resolveHealthState looks up the participant's current health state across // the warmup pools they may belong to. Returns Healthy if the account is not // in any pool or the lookup fails — failing open keeps warmup running rather @@ -155,6 +180,23 @@ func (s *schedulerService) CalculateNextWarmupTime(ctx context.Context, accountI } targetVolume := warmupRampTarget(activelyWarming, account.WarmupBase, account.WarmupIncrease, account.WarmupMax, daysWarming, inCampaign) + // Vary the day's target so a mailbox doesn't send an identical count every + // day. Deterministic per (account, local day) so it's stable across the + // day's reschedules. Actively-warming mailboxes keep a floor of WarmupBase. + if activelyWarming && targetVolume > 0 { + factor := dailyVolumeFactor(accountID, time.Now().In(loadLocation(account.Timezone))) + varied := int(float64(targetVolume)*factor + 0.5) + if varied < account.WarmupBase { + varied = account.WarmupBase + } + if varied < 1 { + varied = 1 + } + if varied < targetVolume { + targetVolume = varied + } + } + // STEP 2.1: Cap per-mailbox volume to actual recipient capacity. The // sender should not send multiple warmup messages to the same recipient // in a single day just to hit an arbitrary target; that creates obvious