Files
warmbly/internal/infrastructure/db/migrations/000102_send_lifecycle.up.sql
T
Matthew Meszaros 9f23c663c4 feat: give a cold mailbox a rotation lifecycle so a tired one can rest (#237)
* 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
2026-08-28 12:08:52 -07:00

41 lines
2.1 KiB
SQL

-- Cold mailbox lifecycle (issue #157).
--
-- Cold sending ran until a hard health band tripped, with nothing in between:
-- a mailbox showing early fatigue either kept sending at full volume or was
-- quarantined. There was no way to pull one out of cold rotation to recover on
-- warmup traffic alone and put it back once it had.
--
-- This is a different axis from risk_band, which decides WHICH worker and IP
-- host a mailbox. This decides whether the mailbox is offered to cold sending
-- at all; a resting mailbox is still a clean-band mailbox.
--
-- Defaults to 'active' so no existing mailbox changes behaviour on deploy.
ALTER TABLE public.email_accounts
ADD COLUMN send_lifecycle text NOT NULL DEFAULT 'active',
-- When the current state was entered, so a rest has a measurable length
-- and promotion can require a probation window rather than a sweep tick.
ADD COLUMN send_lifecycle_since timestamptz,
ADD COLUMN send_lifecycle_reason text,
-- When the rebalancer last looked at this mailbox. Ordering candidates by
-- it guarantees rotation: ordering by send_lifecycle_since alone means
-- every never-moved mailbox sorts equal-first, so on an install with more
-- than one page of them the same page is re-examined forever and the rest
-- are never evaluated at all.
ADD COLUMN send_lifecycle_checked_at timestamptz;
ALTER TABLE public.email_accounts
ADD CONSTRAINT email_accounts_send_lifecycle_check
CHECK (send_lifecycle IN ('warming', 'active', 'resting', 'reserve'));
-- Cold sender resolution filters on this every scheduling pass.
CREATE INDEX idx_email_accounts_send_lifecycle
ON public.email_accounts USING btree (send_lifecycle)
WHERE send_lifecycle <> 'active';
-- The rebalancer's rotation order.
CREATE INDEX idx_email_accounts_lifecycle_checked
ON public.email_accounts USING btree (send_lifecycle_checked_at NULLS FIRST);
COMMENT ON COLUMN public.email_accounts.send_lifecycle IS
'Whether the mailbox is offered to cold sending: warming | active | resting | reserve. Orthogonal to risk_band, which decides which worker hosts it.';