feat(PROJ-56): Last-Entzerrung für OCR und IMAP-Sync
OCR-Worker pausieren optional in konfigurierbarem Zeitfenster (paused_hours), Jobs bleiben pending statt verworfen zu werden. IMAP-Scheduler verteilt Sync-Starts via deterministischem Pro-Account-Jitter, um Lastspitzen bei vielen Postfächern mit gleichem Intervall zu vermeiden. Beides per Config opt-out, Default-Verhalten unverändert. Build + Smoke-Test auf 132 verifiziert.
This commit is contained in:
@@ -26,6 +26,11 @@ type Scheduler struct {
|
||||
mu sync.Mutex
|
||||
running map[int64]bool // in-memory guard against concurrent syncs
|
||||
|
||||
// PROJ-56: deterministic per-account jitter window in seconds. 0 disables
|
||||
// jitter. The offset for an account is derived solely from its ID so the
|
||||
// effective sync time is stable across ticks (no rand() per tick).
|
||||
jitterSeconds int
|
||||
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
@@ -39,6 +44,32 @@ func NewScheduler(store *Store, importer *Importer, logger *slog.Logger) *Schedu
|
||||
}
|
||||
}
|
||||
|
||||
// SetJitterSeconds configures the deterministic per-account sync jitter window
|
||||
// (PROJ-56). 0 disables jitter (legacy behaviour: sync exactly at interval).
|
||||
// Negative values are clamped to 0.
|
||||
func (s *Scheduler) SetJitterSeconds(seconds int) {
|
||||
if seconds < 0 {
|
||||
seconds = 0
|
||||
}
|
||||
s.jitterSeconds = seconds
|
||||
}
|
||||
|
||||
// jitterOffset returns the deterministic delay added on top of the sync
|
||||
// interval for a given account. The offset is in [0, jitterSeconds) and
|
||||
// depends only on the account ID, so it never changes between ticks.
|
||||
func (s *Scheduler) jitterOffset(accountID int64) time.Duration {
|
||||
if s.jitterSeconds <= 0 {
|
||||
return 0
|
||||
}
|
||||
// Account IDs are positive sequential integers; a simple modulo spreads
|
||||
// them evenly across the window. Use the absolute value defensively.
|
||||
id := accountID
|
||||
if id < 0 {
|
||||
id = -id
|
||||
}
|
||||
return time.Duration(id%int64(s.jitterSeconds)) * time.Second
|
||||
}
|
||||
|
||||
// SetAuditLogger wires an audit.Logger into the scheduler so that
|
||||
// UIDVALIDITY-reset events (PROJ-45) are persisted as tenant-visible
|
||||
// audit entries. Optional — when nil, only structured logs are emitted.
|
||||
@@ -123,7 +154,12 @@ func (s *Scheduler) checkAccounts(ctx context.Context) {
|
||||
lastSync = *acc.LastSyncAt
|
||||
}
|
||||
|
||||
if now.Sub(lastSync) >= interval {
|
||||
// PROJ-56: spread the actual sync start with a deterministic per-account
|
||||
// offset so accounts sharing an interval don't all poll on the same
|
||||
// minute boundary. Offset is 0 when jitter is disabled.
|
||||
dueAfter := interval + s.jitterOffset(acc.ID)
|
||||
|
||||
if now.Sub(lastSync) >= dueAfter {
|
||||
s.mu.Lock()
|
||||
s.running[acc.ID] = true
|
||||
s.mu.Unlock()
|
||||
|
||||
Reference in New Issue
Block a user