feat(server/m7-recurring-scans): TryStartScan helper + reaper
Extracts the in-flight check + scan-start logic from handleTriggerScan into a shared library.TryStartScan helper used by both manual and (future) scheduled paths. Folds in the lazy 1-hour stuck-scan reaper: in-flight rows older than StuckScanThreshold get FinishScanRun-ed with error_message="reaped (stale)" and the new scan proceeds. handleTriggerScan becomes a thin wrapper preserving the same external HTTP behavior (202/409/500). Operators no longer need to manually clear stuck rows after a server crash — the next manual trigger reaps it. Tests gated on MINSTREL_TEST_DATABASE_URL: no-in-flight starts; fresh-in-flight skips with row pointer; stale-in-flight reaps (verified via finished_at + error_message); DB error propagates.
This commit is contained in:
+14
-20
@@ -74,30 +74,24 @@ type scanTriggerResp struct {
|
||||
}
|
||||
|
||||
// handleTriggerScan implements POST /api/admin/scan/run.
|
||||
// Returns 202 on success, 409 if a scan is already in flight.
|
||||
// The scan runs in a background goroutine detached from the request context.
|
||||
// Returns 202 on success, 409 if a scan is already in flight (and not stale),
|
||||
// 500 on DB errors. The lazy reaper inside library.TryStartScan handles
|
||||
// stale (>1h) in-flight rows automatically — the operator no longer needs
|
||||
// to manually clear stuck rows after a server crash.
|
||||
func (h *handlers) handleTriggerScan(w http.ResponseWriter, _ *http.Request) {
|
||||
bgCtx := context.Background()
|
||||
q := dbq.New(h.pool)
|
||||
|
||||
existing, err := q.GetInFlightScanRun(bgCtx)
|
||||
if err == nil {
|
||||
started, existing, err := library.TryStartScan(
|
||||
bgCtx, h.pool, h.scanner, h.coverart,
|
||||
h.logger.With("source", "manual"), h.scanCfg,
|
||||
)
|
||||
if err != nil {
|
||||
h.logger.Error("admin: trigger scan failed", "err", err)
|
||||
writeErr(w, http.StatusInternalServerError, "server_error", "trigger failed")
|
||||
return
|
||||
}
|
||||
if !started {
|
||||
writeJSON(w, http.StatusConflict, scanTriggerResp{ID: uuidToString(existing.ID)})
|
||||
return
|
||||
}
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
h.logger.Error("admin: in-flight check failed", "err", err)
|
||||
writeErr(w, http.StatusInternalServerError, "server_error", "in-flight check failed")
|
||||
return
|
||||
}
|
||||
|
||||
// Detached goroutine — must outlive the HTTP request.
|
||||
go func() {
|
||||
if _, err := library.RunScan(bgCtx, h.pool, h.scanner, h.coverart,
|
||||
h.logger.With("component", "scan_run"), h.scanCfg); err != nil {
|
||||
h.logger.Warn("manual scan run failed", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
writeJSON(w, http.StatusAccepted, scanTriggerResp{})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user