package jobs import "sync" // DefaultSyncWorkers is the in-process bound for concurrent feed/Woo/Shopify syncs. // Claim paths use FOR UPDATE SKIP LOCKED so each slot gets a distinct job. const DefaultSyncWorkers = 1 // MaxSyncWorkers caps in-process sync parallelism (DB pool + upstream API RPM). const MaxSyncWorkers = 2 // ClampSyncWorkers bounds n to [1, MaxSyncWorkers]. func ClampSyncWorkers(n int) int { if n < 1 { return 1 } if n > MaxSyncWorkers { return MaxSyncWorkers } return n } // SyncSlots limits concurrent sync Process* goroutines across feed/Woo/Shopify claims. type SyncSlots struct { Workers int sem chan struct{} wg sync.WaitGroup } // NewSyncSlots creates a bounded slot set for concurrent sync jobs. func NewSyncSlots(workers int) *SyncSlots { w := ClampSyncWorkers(workers) return &SyncSlots{ Workers: w, sem: make(chan struct{}, w), } } // Wait blocks until all in-flight sync goroutines finish. func (s *SyncSlots) Wait() { s.wg.Wait() } // TryStart claims one free slot (non-blocking). claim must be SKIP LOCKED–safe. // If claim fails, the slot is released. On success, run executes in a new goroutine. func (s *SyncSlots) TryStart(claim func() error, run func()) (started bool, claimErr error) { select { case s.sem <- struct{}{}: default: return false, nil } if err := claim(); err != nil { <-s.sem return false, err } s.wg.Add(1) go func() { defer s.wg.Done() defer func() { <-s.sem }() run() }() return true, nil }