feat(PROJ-56): OCR-Pausenfenster per SIGHUP-Reload statt Restart

paused_hours konnte bisher nur über einen vollen Prozess-Restart geändert
werden, was SMTP/IMAP/API unnötig unterbricht. Worker.pausedHours ist jetzt
ein atomic.Pointer mit SetPausedHours(); SIGHUP liest config.yml neu und
aktualisiert nur die OCR-Pausenzeit im laufenden Prozess. Neue
deploy/cron.d/archivmail-ocr-pause(.sh) lässt Admins die Pausenzeiten direkt
in der Cron-Datei pflegen und löst per systemctl reload statt restart aus.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-06-24 14:46:13 +02:00
co-authored by Claude Sonnet 4.6
parent be48a99af9
commit dba9939880
5 changed files with 119 additions and 12 deletions
+26 -12
View File
@@ -6,6 +6,7 @@ import (
"log/slog"
"strings"
"sync"
"sync/atomic"
"time"
"archivmail/internal/index"
@@ -38,7 +39,11 @@ type Worker struct {
// 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
//
// Stored as atomic.Pointer so SetPausedHours can be called concurrently
// from a signal handler (SIGHUP reload) while run() goroutines read it,
// without requiring a process restart to change the window (PROJ-56b).
pausedHours atomic.Pointer[[2]int]
}
// pauseCheckInterval is how often a paused worker re-checks whether the pause
@@ -73,26 +78,35 @@ func NewWorker(store *storage.Store, idxMgr index.TenantIndexer, opts Options) *
if opts.Logger == nil {
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,
pausedHours: opts.PausedHours,
w := &Worker{
store: store,
idxMgr: idxMgr,
logger: opts.Logger,
queue: make(chan Job, opts.QueueSize),
done: make(chan struct{}),
workers: opts.Workers,
langs: opts.Langs,
}
w.pausedHours.Store(opts.PausedHours)
return w
}
// SetPausedHours updates the pause window at runtime, without requiring a
// worker/process restart. Pass nil to disable pausing. Safe to call
// concurrently with running workers (e.g. from a SIGHUP reload handler).
func (w *Worker) SetPausedHours(hours *[2]int) {
w.pausedHours.Store(hours)
}
// 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 {
hours := w.pausedHours.Load()
if hours == nil {
return false
}
start, end := w.pausedHours[0], w.pausedHours[1]
start, end := hours[0], hours[1]
if start == end {
// Degenerate / no-op window — never pause.
return false