mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-10 08:06:04 +00:00
* feat: give a cold mailbox a rotation lifecycle so a tired one can rest and come back, instead of running at full volume until a hard band trips: send_lifecycle is warming, active, resting or reserve and decides whether cold sender resolution offers the mailbox at all, which is a different axis from risk_band deciding which worker and IP host it, so a resting mailbox is still a clean-band mailbox that keeps its warmup traffic and its reputation; the hourly rebalancer rests a mailbox at throttled and worse but never at watch, since watch is defined as the band that changes nothing a customer can feel and leaving cold rotation is very much something they feel, and a rested mailbox returns only after three clean days so one good hour cannot bounce it back to full volume; reserve is the owner's hold and is never overridden, the default is active so no existing mailbox changes on deploy, and the state never travels in a workspace archive because it is this instance's decision about sending it watched * feat: stop a query error re-admitting rested mailboxes, make probation measure healthy time, and rotate the candidate window so no mailbox starves: sendLifecycles returned a nil map on failure and an unresolved state reads as active, so one bad query quietly put every resting and reserved mailbox back into cold rotation, and the gate is now applied only when the states were actually read, with the skip logged rather than silent; ReadyToResume measured total time resting, so a mailbox that sat unhealthy for three days resumed on its first healthy tick having served no clean time, and an unhealthy evaluation now restarts the streak; and ordering candidates by send_lifecycle_since put every never-moved mailbox equal-first, so on an install with more than one page of them the same page was re-examined forever, which a checked-at stamp and its index fix
76 lines
2.3 KiB
Go
76 lines
2.3 KiB
Go
package models
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestSendsCold(t *testing.T) {
|
|
if !SendLifecycleActive.SendsCold() {
|
|
t.Error("active must send cold")
|
|
}
|
|
// A row written before the column existed reads as empty. Treating that as
|
|
// "not sending" would silently stop every existing mailbox on deploy.
|
|
if !SendLifecycle("").SendsCold() {
|
|
t.Error("an unset lifecycle must send cold")
|
|
}
|
|
for _, l := range []SendLifecycle{SendLifecycleWarming, SendLifecycleResting, SendLifecycleReserve} {
|
|
if l.SendsCold() {
|
|
t.Errorf("%q must not be offered cold traffic", l)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAutoManaged(t *testing.T) {
|
|
// Reserve is the owner's decision; the rebalancer must never take a
|
|
// mailbox out of it.
|
|
if SendLifecycleReserve.AutoManaged() {
|
|
t.Error("reserve must not be automatically managed")
|
|
}
|
|
// Warming follows the mailbox's own warmup settings, not the rebalancer.
|
|
if SendLifecycleWarming.AutoManaged() {
|
|
t.Error("warming must not be automatically managed")
|
|
}
|
|
for _, l := range []SendLifecycle{SendLifecycleActive, SendLifecycleResting, ""} {
|
|
if !l.AutoManaged() {
|
|
t.Errorf("%q should be automatically managed", l)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReadyToResume(t *testing.T) {
|
|
now := time.Date(2026, 3, 10, 12, 0, 0, 0, time.UTC)
|
|
since := now.Add(-RestProbation)
|
|
|
|
rested := SendLifecycleState{State: SendLifecycleResting, Since: &since}
|
|
if !rested.ReadyToResume(now) {
|
|
t.Error("a mailbox that served its full probation should be ready")
|
|
}
|
|
|
|
fresh := now.Add(-time.Hour)
|
|
if (SendLifecycleState{State: SendLifecycleResting, Since: &fresh}).ReadyToResume(now) {
|
|
t.Error("an hour of rest is not a probation")
|
|
}
|
|
// No timestamp means nothing to measure, so it is not ready: promoting on a
|
|
// missing clock would defeat the probation entirely.
|
|
if (SendLifecycleState{State: SendLifecycleResting}).ReadyToResume(now) {
|
|
t.Error("a resting mailbox with no start time must not resume")
|
|
}
|
|
if (SendLifecycleState{State: SendLifecycleActive, Since: &since}).ReadyToResume(now) {
|
|
t.Error("only a resting mailbox resumes")
|
|
}
|
|
}
|
|
|
|
func TestValid(t *testing.T) {
|
|
for _, l := range []SendLifecycle{SendLifecycleWarming, SendLifecycleActive, SendLifecycleResting, SendLifecycleReserve} {
|
|
if !l.Valid() {
|
|
t.Errorf("%q should be valid", l)
|
|
}
|
|
}
|
|
for _, l := range []SendLifecycle{"", "paused", "nonsense"} {
|
|
if l.Valid() {
|
|
t.Errorf("%q should not be valid", l)
|
|
}
|
|
}
|
|
}
|