mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-09 16:04:41 +00:00
feat: cap warmup for active campaign senders
Limit warmup volume to 5 messages per day when a mailbox is part of an active campaign sender pool. This keeps warmup as a low-volume reputation heartbeat while campaign traffic is already active.
This commit is contained in:
@@ -37,6 +37,7 @@ type CampaignRepository interface {
|
||||
ValidateCampaignReady(ctx context.Context, campaignID uuid.UUID) error
|
||||
GetPendingCampaignTasks(ctx context.Context, campaignID uuid.UUID) ([]Task, error)
|
||||
CountActiveForOrganization(ctx context.Context, orgID uuid.UUID) (int, error)
|
||||
AccountHasActiveCampaign(ctx context.Context, accountID uuid.UUID) (bool, error)
|
||||
}
|
||||
|
||||
type campaignRepository struct {
|
||||
@@ -1020,6 +1021,24 @@ func (r *campaignRepository) CountActiveForOrganization(ctx context.Context, org
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *campaignRepository) AccountHasActiveCampaign(ctx context.Context, accountID uuid.UUID) (bool, error) {
|
||||
query := `
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM email_accounts ea
|
||||
JOIN email_tags et ON et.email_id = ea.id
|
||||
JOIN campaign_email_tags cet ON cet.tag_id = et.tag_id
|
||||
JOIN campaigns c ON c.id = cet.campaign_id
|
||||
WHERE ea.id = $1
|
||||
AND ea.status = 'active'
|
||||
AND c.status = 'active'
|
||||
)
|
||||
`
|
||||
var exists bool
|
||||
err := r.DB.QueryRow(ctx, query, accountID).Scan(&exists)
|
||||
return exists, err
|
||||
}
|
||||
|
||||
// UpdateStatusWithLock updates campaign status using a PostgreSQL advisory lock to prevent concurrent updates.
|
||||
// The WHERE clause guards against races: only updates if the campaign is currently 'active'.
|
||||
func (r *campaignRepository) UpdateStatusWithLock(ctx context.Context, campaignID uuid.UUID, status string) error {
|
||||
|
||||
@@ -15,6 +15,7 @@ var poolTypesForHealthLookup = []string{"premium", "free"}
|
||||
const (
|
||||
minWarmupRecipientRecheck = 4 * time.Hour
|
||||
maxWarmupRecipientRecheck = 8 * time.Hour
|
||||
activeCampaignWarmupCap = 5
|
||||
)
|
||||
|
||||
// healthAdjustment captures how the throttled/watch health state should
|
||||
@@ -112,6 +113,16 @@ func (s *schedulerService) CalculateNextWarmupTime(ctx context.Context, accountI
|
||||
account.WarmupMax,
|
||||
)
|
||||
|
||||
// Active campaign mailboxes already have production sending pressure.
|
||||
// Keep warmup as a lightweight reputation heartbeat instead of stacking
|
||||
// the full warmup ramp on top of campaign traffic.
|
||||
if s.campaignRepo != nil {
|
||||
hasActiveCampaign, err := s.campaignRepo.AccountHasActiveCampaign(ctx, accountID)
|
||||
if err == nil && hasActiveCampaign && targetVolume > activeCampaignWarmupCap {
|
||||
targetVolume = activeCampaignWarmupCap
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user