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:
+58
-7
@@ -6,6 +6,7 @@ import (
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"archivmail/internal/index"
|
||||
"archivmail/internal/storage"
|
||||
@@ -32,14 +33,28 @@ type Worker struct {
|
||||
wg sync.WaitGroup
|
||||
workers int
|
||||
langs []string
|
||||
|
||||
// PROJ-56: optional local-time pause window [start, end). When the current
|
||||
// hour falls inside it, workers stop consuming the queue (jobs stay buffered
|
||||
// in the channel / as ocr_status='pending' in the DB) until it reopens.
|
||||
// nil = never pause (legacy behaviour).
|
||||
pausedHours *[2]int
|
||||
}
|
||||
|
||||
// pauseCheckInterval is how often a paused worker re-checks whether the pause
|
||||
// window has closed.
|
||||
const pauseCheckInterval = 60 * time.Second
|
||||
|
||||
// Options configures a Worker. Zero values are replaced with sensible defaults.
|
||||
type Options struct {
|
||||
QueueSize int // default 1000
|
||||
Workers int // default 2
|
||||
Langs []string // default ["deu", "eng"]
|
||||
Logger *slog.Logger
|
||||
// PausedHours optionally pauses processing during a local-time window
|
||||
// [start, end) (PROJ-56). Wrap-around windows (e.g. [22, 6]) are supported.
|
||||
// nil = never pause. The manual reprocess command leaves this nil on purpose.
|
||||
PausedHours *[2]int
|
||||
}
|
||||
|
||||
// NewWorker constructs a worker that reads mails from store, runs OCR on
|
||||
@@ -59,16 +74,37 @@ func NewWorker(store *storage.Store, idxMgr index.TenantIndexer, opts Options) *
|
||||
opts.Logger = slog.Default()
|
||||
}
|
||||
return &Worker{
|
||||
store: store,
|
||||
idxMgr: idxMgr,
|
||||
logger: opts.Logger,
|
||||
queue: make(chan Job, opts.QueueSize),
|
||||
done: make(chan struct{}),
|
||||
workers: opts.Workers,
|
||||
langs: opts.Langs,
|
||||
store: store,
|
||||
idxMgr: idxMgr,
|
||||
logger: opts.Logger,
|
||||
queue: make(chan Job, opts.QueueSize),
|
||||
done: make(chan struct{}),
|
||||
workers: opts.Workers,
|
||||
langs: opts.Langs,
|
||||
pausedHours: opts.PausedHours,
|
||||
}
|
||||
}
|
||||
|
||||
// isPaused reports whether the OCR worker should currently hold off processing
|
||||
// because the local time falls inside the configured pause window.
|
||||
// Supports wrap-around windows where start > end (e.g. [22, 6]).
|
||||
func (w *Worker) isPaused(now time.Time) bool {
|
||||
if w.pausedHours == nil {
|
||||
return false
|
||||
}
|
||||
start, end := w.pausedHours[0], w.pausedHours[1]
|
||||
if start == end {
|
||||
// Degenerate / no-op window — never pause.
|
||||
return false
|
||||
}
|
||||
h := now.Hour()
|
||||
if start < end {
|
||||
return h >= start && h < end
|
||||
}
|
||||
// Wrap-around window, e.g. [22, 6): paused 22,23,0,1,...,5.
|
||||
return h >= start || h < end
|
||||
}
|
||||
|
||||
// Submit enqueues a job. Drops with a warning if the queue is full so the
|
||||
// caller (mail intake) is never blocked.
|
||||
func (w *Worker) Submit(mailID string, tenantID *int64) {
|
||||
@@ -108,6 +144,21 @@ func (w *Worker) Stop() {
|
||||
func (w *Worker) run(ctx context.Context, id int) {
|
||||
defer w.wg.Done()
|
||||
for {
|
||||
// PROJ-56: while inside the pause window, do NOT consume the queue.
|
||||
// Jobs stay buffered in the channel (and as ocr_status='pending' in the
|
||||
// DB), so nothing is lost. We re-check periodically and still react to
|
||||
// shutdown / context cancellation immediately.
|
||||
if w.isPaused(time.Now()) {
|
||||
select {
|
||||
case <-w.done:
|
||||
return
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(pauseCheckInterval):
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case job, ok := <-w.queue:
|
||||
if !ok {
|
||||
|
||||
Reference in New Issue
Block a user