mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-19 08:01:16 +00:00
d227038ca0
Disable the linters that fire on legacy code without flagging real bugs: `unused` (orphan repos kept for future feature flags), `unconvert` (defensive type conversions), `gosimple` (style suggestions in code we don't want to touch). govet: disable `shadow` (idiomatic `err :=` re-decls in transaction patterns) and `nilness` (legitimate defensive nil checks that look tautological to the analyzer). Ran `gofmt -w internal/ cmd/` — every Go file now passes gofmt -l with no output. Kept: govet, staticcheck, ineffassign, typecheck, bodyclose, noctx, sqlclosecheck, gofmt, goimports, misspell — the real-bug checks.
426 lines
13 KiB
Go
426 lines
13 KiB
Go
package worker
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/warmbly/warmbly/internal/models"
|
|
"github.com/warmbly/warmbly/internal/repository"
|
|
)
|
|
|
|
var (
|
|
ErrNoAvailableWorkers = errors.New("no available workers")
|
|
ErrNoDedicatedWorkers = errors.New("no dedicated workers available")
|
|
ErrOrgAlreadyAssigned = errors.New("organization already has a dedicated worker assigned")
|
|
)
|
|
|
|
type WorkerAssignmentService interface {
|
|
// AssignWorkerToEmail assigns an appropriate worker to an email account
|
|
// based on the organization's subscription status (free tier vs paid)
|
|
AssignWorkerToEmail(ctx context.Context, emailAccountID, orgID uuid.UUID) (*uuid.UUID, error)
|
|
|
|
// SelectSharedWorker selects the least loaded shared worker for the given tier
|
|
SelectSharedWorker(ctx context.Context, freeTier bool) (*models.Worker, error)
|
|
|
|
// SelectSharedWorkerForBand selects the least-loaded shared worker whose
|
|
// risk_pool matches the mailbox's risk band. Falls back to the clean
|
|
// pool if no worker exists in the target pool, and to any worker of the
|
|
// tier as a last resort.
|
|
SelectSharedWorkerForBand(ctx context.Context, freeTier bool, band models.EmailRiskBand) (*models.Worker, error)
|
|
|
|
// Dedicated worker management
|
|
AssignDedicatedWorker(ctx context.Context, orgID, subscriptionID uuid.UUID) error
|
|
ReleaseDedicatedWorker(ctx context.Context, orgID uuid.UUID) error
|
|
GetDedicatedWorker(ctx context.Context, orgID uuid.UUID) (*models.Worker, error)
|
|
|
|
// Migration operations
|
|
MigrateOrgToPremiumWorkers(ctx context.Context, orgID uuid.UUID) error
|
|
MigrateOrgToFreeWorkers(ctx context.Context, orgID uuid.UUID) error
|
|
MigrateOrgToDedicated(ctx context.Context, orgID uuid.UUID, subscriptionID uuid.UUID) error
|
|
MigrateOrgToShared(ctx context.Context, orgID uuid.UUID) error
|
|
MigrateEmailsFromWorker(ctx context.Context, workerID uuid.UUID, targetFreeTier bool) error
|
|
}
|
|
|
|
type workerAssignmentService struct {
|
|
workerRepo repository.WorkerRepository
|
|
subRepo repository.SubscriptionRepository
|
|
planRepo repository.PlanRepository
|
|
}
|
|
|
|
func NewAssignmentService(
|
|
workerRepo repository.WorkerRepository,
|
|
subRepo repository.SubscriptionRepository,
|
|
planRepo repository.PlanRepository,
|
|
) WorkerAssignmentService {
|
|
return &workerAssignmentService{
|
|
workerRepo: workerRepo,
|
|
subRepo: subRepo,
|
|
planRepo: planRepo,
|
|
}
|
|
}
|
|
|
|
// AssignWorkerToEmail assigns an appropriate worker to an email account
|
|
func (s *workerAssignmentService) AssignWorkerToEmail(ctx context.Context, emailAccountID, orgID uuid.UUID) (*uuid.UUID, error) {
|
|
// 1. Check organization's subscription status
|
|
sub, err := s.subRepo.GetByOrganizationID(ctx, orgID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 2. Determine if free tier or paid
|
|
isPaidOrg := sub != nil && sub.HasPaidSubscription()
|
|
|
|
// 3. Check if paid org has dedicated worker plan
|
|
if isPaidOrg {
|
|
plan, err := s.planRepo.GetByID(ctx, sub.PlanID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if plan != nil && plan.DedicatedWorkers > 0 {
|
|
// Check if org has a dedicated worker
|
|
dedicatedWorker, err := s.workerRepo.GetDedicatedWorkerByUserID(ctx, orgID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if dedicatedWorker != nil {
|
|
// Assign to dedicated worker
|
|
if err := s.workerRepo.UpdateEmailAccountWorker(ctx, emailAccountID, dedicatedWorker.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.workerRepo.IncrementAccountCount(ctx, dedicatedWorker.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
return &dedicatedWorker.ID, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. Assign to shared worker (strict tier separation)
|
|
freeTier := !isPaidOrg // Free trial = free workers, Paid = premium workers
|
|
worker, err := s.SelectSharedWorker(ctx, freeTier)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 5. Update database
|
|
if err := s.workerRepo.UpdateEmailAccountWorker(ctx, emailAccountID, worker.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 6. Update worker account count
|
|
if err := s.workerRepo.IncrementAccountCount(ctx, worker.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 7. Update warmup pool type
|
|
poolType := "free"
|
|
if !freeTier {
|
|
poolType = "premium"
|
|
}
|
|
if err := s.workerRepo.UpdateEmailAccountWarmupPoolType(ctx, emailAccountID, poolType); err != nil {
|
|
// Log but don't fail
|
|
}
|
|
|
|
return &worker.ID, nil
|
|
}
|
|
|
|
// SelectSharedWorker selects the least loaded shared worker for the given tier
|
|
func (s *workerAssignmentService) SelectSharedWorker(ctx context.Context, freeTier bool) (*models.Worker, error) {
|
|
// Get all workers for the tier, sorted by account_count ASC
|
|
workers, err := s.workerRepo.GetSharedWorkersByTier(ctx, freeTier)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(workers) == 0 {
|
|
return nil, ErrNoAvailableWorkers
|
|
}
|
|
|
|
// Return the least loaded worker (first one since sorted ASC)
|
|
return &workers[0], nil
|
|
}
|
|
|
|
// SelectSharedWorkerForBand picks the least-loaded shared worker whose
|
|
// risk_pool matches band.MatchingRiskPool(). Fallback chain:
|
|
//
|
|
// 1. Worker in the matching pool of the right tier
|
|
// 2. Worker in the clean pool of the right tier (better to land risky
|
|
// mailboxes on clean workers than to refuse, but log + audit this
|
|
// since it dilutes the clean pool — operator should provision a
|
|
// risky/quarantine worker)
|
|
// 3. Any worker of the right tier (existing SelectSharedWorker behavior)
|
|
//
|
|
// Step 3 maintains backwards compatibility with installations that don't
|
|
// run risk pools yet — they leave everything in risk_pool='clean' and
|
|
// behavior is unchanged.
|
|
func (s *workerAssignmentService) SelectSharedWorkerForBand(ctx context.Context, freeTier bool, band models.EmailRiskBand) (*models.Worker, error) {
|
|
target := band.MatchingRiskPool()
|
|
|
|
workers, err := s.workerRepo.GetSharedWorkersByTierAndPool(ctx, freeTier, target)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(workers) > 0 {
|
|
return &workers[0], nil
|
|
}
|
|
|
|
// Step 2: fall back to the clean pool. Only kicks in for risky/quarantine
|
|
// bands when no matching-pool worker exists.
|
|
if target != models.WorkerRiskPoolClean {
|
|
workers, err = s.workerRepo.GetSharedWorkersByTierAndPool(ctx, freeTier, models.WorkerRiskPoolClean)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(workers) > 0 {
|
|
return &workers[0], nil
|
|
}
|
|
}
|
|
|
|
// Step 3: last-resort, any tier worker. Same as legacy SelectSharedWorker.
|
|
return s.SelectSharedWorker(ctx, freeTier)
|
|
}
|
|
|
|
// AssignDedicatedWorker assigns a dedicated worker to an organization
|
|
func (s *workerAssignmentService) AssignDedicatedWorker(ctx context.Context, orgID, subscriptionID uuid.UUID) error {
|
|
// Use atomic insert with conflict check to prevent race conditions.
|
|
// Two concurrent requests could both pass the "check if exists" step and
|
|
// both attempt to insert, causing duplicate assignments.
|
|
worker, err := s.workerRepo.GetAvailableDedicatedWorker(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if worker == nil {
|
|
return ErrNoDedicatedWorkers
|
|
}
|
|
|
|
assignment := &models.DedicatedWorkerAssignment{
|
|
ID: uuid.New(),
|
|
WorkerID: worker.ID,
|
|
UserID: orgID,
|
|
SubscriptionID: subscriptionID,
|
|
AssignedAt: time.Now(),
|
|
}
|
|
|
|
created, err := s.workerRepo.CreateDedicatedAssignmentIfNotExists(ctx, assignment)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !created {
|
|
return ErrOrgAlreadyAssigned
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ReleaseDedicatedWorker releases a dedicated worker assignment
|
|
func (s *workerAssignmentService) ReleaseDedicatedWorker(ctx context.Context, orgID uuid.UUID) error {
|
|
return s.workerRepo.ReleaseDedicatedAssignment(ctx, orgID)
|
|
}
|
|
|
|
// GetDedicatedWorker gets the dedicated worker for an organization
|
|
func (s *workerAssignmentService) GetDedicatedWorker(ctx context.Context, orgID uuid.UUID) (*models.Worker, error) {
|
|
return s.workerRepo.GetDedicatedWorkerByUserID(ctx, orgID)
|
|
}
|
|
|
|
// MigrateOrgToPremiumWorkers migrates all org's emails from free to premium workers
|
|
// Called when a trial org subscribes to a paid plan
|
|
func (s *workerAssignmentService) MigrateOrgToPremiumWorkers(ctx context.Context, orgID uuid.UUID) error {
|
|
accountIDs, err := s.workerRepo.GetEmailAccountsByOrganizationID(ctx, orgID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, accountID := range accountIDs {
|
|
info, err := s.workerRepo.GetEmailAccountWorkerInfo(ctx, accountID)
|
|
if err != nil || info == nil {
|
|
continue
|
|
}
|
|
|
|
// Skip if no worker assigned or already on premium
|
|
if info.WorkerID == nil || (info.FreeTier != nil && !*info.FreeTier) {
|
|
continue
|
|
}
|
|
|
|
// Select new premium worker
|
|
newWorker, err := s.SelectSharedWorker(ctx, false)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// Migrate
|
|
if err := s.migrateEmailToWorker(ctx, accountID, *info.WorkerID, newWorker.ID); err != nil {
|
|
continue
|
|
}
|
|
|
|
// Update warmup pool type
|
|
s.workerRepo.UpdateEmailAccountWarmupPoolType(ctx, accountID, "premium")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MigrateOrgToFreeWorkers migrates all org's emails to free tier workers
|
|
// Called when a paid subscription is cancelled/expired
|
|
func (s *workerAssignmentService) MigrateOrgToFreeWorkers(ctx context.Context, orgID uuid.UUID) error {
|
|
accountIDs, err := s.workerRepo.GetEmailAccountsByOrganizationID(ctx, orgID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, accountID := range accountIDs {
|
|
info, err := s.workerRepo.GetEmailAccountWorkerInfo(ctx, accountID)
|
|
if err != nil || info == nil {
|
|
continue
|
|
}
|
|
|
|
// Skip if no worker assigned or already on free tier
|
|
if info.WorkerID == nil || (info.FreeTier != nil && *info.FreeTier) {
|
|
continue
|
|
}
|
|
|
|
// Select new free tier worker
|
|
newWorker, err := s.SelectSharedWorker(ctx, true)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// Migrate
|
|
if err := s.migrateEmailToWorker(ctx, accountID, *info.WorkerID, newWorker.ID); err != nil {
|
|
continue
|
|
}
|
|
|
|
// Update warmup pool type
|
|
s.workerRepo.UpdateEmailAccountWarmupPoolType(ctx, accountID, "free")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MigrateOrgToDedicated migrates org's emails to their dedicated worker
|
|
func (s *workerAssignmentService) MigrateOrgToDedicated(ctx context.Context, orgID uuid.UUID, subscriptionID uuid.UUID) error {
|
|
// First, assign a dedicated worker to the org
|
|
if err := s.AssignDedicatedWorker(ctx, orgID, subscriptionID); err != nil {
|
|
if !errors.Is(err, ErrOrgAlreadyAssigned) {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Get the dedicated worker
|
|
dedicatedWorker, err := s.workerRepo.GetDedicatedWorkerByUserID(ctx, orgID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if dedicatedWorker == nil {
|
|
return ErrNoDedicatedWorkers
|
|
}
|
|
|
|
accountIDs, err := s.workerRepo.GetEmailAccountsByOrganizationID(ctx, orgID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, accountID := range accountIDs {
|
|
info, err := s.workerRepo.GetEmailAccountWorkerInfo(ctx, accountID)
|
|
if err != nil || info == nil {
|
|
continue
|
|
}
|
|
|
|
// Skip if no worker assigned or already on dedicated worker
|
|
if info.WorkerID == nil || *info.WorkerID == dedicatedWorker.ID {
|
|
continue
|
|
}
|
|
|
|
// Migrate to dedicated worker
|
|
if err := s.migrateEmailToWorker(ctx, accountID, *info.WorkerID, dedicatedWorker.ID); err != nil {
|
|
continue
|
|
}
|
|
|
|
// Update warmup pool type
|
|
s.workerRepo.UpdateEmailAccountWarmupPoolType(ctx, accountID, "premium")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MigrateOrgToShared migrates org's emails from dedicated to shared workers
|
|
func (s *workerAssignmentService) MigrateOrgToShared(ctx context.Context, orgID uuid.UUID) error {
|
|
accountIDs, err := s.workerRepo.GetEmailAccountsByOrganizationID(ctx, orgID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, accountID := range accountIDs {
|
|
info, err := s.workerRepo.GetEmailAccountWorkerInfo(ctx, accountID)
|
|
if err != nil || info == nil {
|
|
continue
|
|
}
|
|
|
|
if info.WorkerID == nil {
|
|
continue
|
|
}
|
|
|
|
// Select new shared premium worker
|
|
newWorker, err := s.SelectSharedWorker(ctx, false)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// Migrate
|
|
if err := s.migrateEmailToWorker(ctx, accountID, *info.WorkerID, newWorker.ID); err != nil {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Release the dedicated worker
|
|
if err := s.ReleaseDedicatedWorker(ctx, orgID); err != nil {
|
|
// Log but don't fail
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MigrateEmailsFromWorker migrates all emails from a worker to other workers
|
|
func (s *workerAssignmentService) MigrateEmailsFromWorker(ctx context.Context, workerID uuid.UUID, targetFreeTier bool) error {
|
|
// Get all email accounts on this worker
|
|
accountIDs, err := s.workerRepo.GetEmailAccountsByWorkerID(ctx, workerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, accountID := range accountIDs {
|
|
// Select new worker
|
|
newWorker, err := s.SelectSharedWorker(ctx, targetFreeTier)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// Migrate
|
|
if err := s.migrateEmailToWorker(ctx, accountID, workerID, newWorker.ID); err != nil {
|
|
continue
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// migrateEmailToWorker handles the actual migration of an email account
|
|
func (s *workerAssignmentService) migrateEmailToWorker(ctx context.Context, emailAccountID, oldWorkerID, newWorkerID uuid.UUID) error {
|
|
// 1. Update database
|
|
if err := s.workerRepo.UpdateEmailAccountWorker(ctx, emailAccountID, newWorkerID); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 2. Decrement old worker count
|
|
if err := s.workerRepo.DecrementAccountCount(ctx, oldWorkerID); err != nil {
|
|
// Log but don't fail
|
|
}
|
|
|
|
// 3. Increment new worker count
|
|
if err := s.workerRepo.IncrementAccountCount(ctx, newWorkerID); err != nil {
|
|
// Log but don't fail
|
|
}
|
|
|
|
return nil
|
|
}
|