Watch
1
0
Fork
You've already forked RedFlag
0

fix: idempotent Stop() + surface discarded errors

sync.Once on TimeoutService/Reconciler/Syncer stop channels.
syncOne returns its error so admin SyncNow reports real failures.
OSV vuln unmarshal failure now logged, still fails closed.
This commit is contained in:
Fimeg 2026-06-11 20:23:35 -04:00
commit 413211c657
4 changed files with 19 additions and 11 deletions

View file

@ -1232,7 +1232,10 @@ func (h *UpdateHandler) evaluateSupplyChainHold(update *models.UpdateState, targ
if osvStatus, osvVulns, err := h.updateQueries.GetVersionOSVStatus(update.PackageType, update.PackageName, targetVersion); err == nil && osvStatus == "vulnerable" {
var vulns []services.VulnerabilityInfo
if len(osvVulns) > 0 {
_ = json.Unmarshal(osvVulns, &vulns)
if err := json.Unmarshal(osvVulns, &vulns); err != nil {
log.Printf("[ERROR] [server] [updates] osv_vulns_unmarshal_failed pkg=%s version=%s error=%v", update.PackageName, targetVersion, err)
// vulns stays nil; block still fires — fail closed on unmarshal error
}
}
return supplyChainHold{blocked: true, reason: fmt.Sprintf("target version %s has known vulnerabilities (version row)", targetVersion), vulns: vulns}
}

View file

@ -29,6 +29,7 @@ type Reconciler struct {
aliasCache *upstream.RepologyCache
interval time.Duration
shutdown chan struct{}
stopOnce sync.Once
mu sync.Mutex
running bool
}
@ -54,7 +55,7 @@ func (r *Reconciler) Start(ctx context.Context) {
}
func (r *Reconciler) Stop() {
close(r.shutdown)
r.stopOnce.Do(func() { close(r.shutdown) })
}
func (r *Reconciler) loop(ctx context.Context) {

View file

@ -3,6 +3,7 @@ package services
import (
"fmt"
"log"
"sync"
"time"
"github.com/Fimeg/RedFlag/server/internal/database/queries"
@ -18,6 +19,7 @@ type TimeoutService struct {
securitySettings *SecuritySettingsService // optional; overrides updateTimeout per-tick via operational.update_stuck_minutes
ticker *time.Ticker
stopChan chan bool
stopOnce sync.Once
sentTimeout time.Duration // For commands already sent to agents
pendingTimeout time.Duration // For commands stuck in queue
receivedTimeout time.Duration // For commands received by agent but not completed (Migration 033 §4)
@ -102,7 +104,7 @@ func (ts *TimeoutService) Start() {
// Stop stops the timeout monitoring service
func (ts *TimeoutService) Stop() {
close(ts.stopChan)
ts.stopOnce.Do(func() { close(ts.stopChan) })
}
// checkForTimeouts checks for commands that have been running too long

View file

@ -2,6 +2,7 @@ package upstream
import (
"context"
"fmt"
"log"
"os"
"strconv"
@ -33,6 +34,7 @@ type Syncer struct {
batch int
syncWorkers int
shutdown chan struct{}
stopOnce sync.Once
}
func NewSyncer(q *queries.UpstreamQueries, r *Registry, interval, staleness time.Duration, batch int, aliasQ *queries.RepologyQueries) *Syncer {
@ -75,7 +77,7 @@ func (s *Syncer) Start(ctx context.Context) {
}
func (s *Syncer) Stop() {
close(s.shutdown)
s.stopOnce.Do(func() { close(s.shutdown) })
}
func (s *Syncer) loop(ctx context.Context) {
@ -163,7 +165,7 @@ func (s *Syncer) tick(ctx context.Context) {
go func() {
defer wg.Done()
defer func() { <-sem }()
s.syncOne(ctx, row)
_ = s.syncOne(ctx, row)
}()
}
wg.Wait()
@ -177,17 +179,16 @@ func (s *Syncer) SyncOne(ctx context.Context, id uuid.UUID) error {
if err != nil {
return err
}
s.syncOne(ctx, *row)
return nil
return s.syncOne(ctx, *row)
}
func (s *Syncer) syncOne(ctx context.Context, row models.TrackedSoftware) {
func (s *Syncer) syncOne(ctx context.Context, row models.TrackedSoftware) error {
source, ok := s.registry.Get(row.Source)
if !ok {
msg := "unknown source: " + row.Source
log.Printf("[WARN] [upstream] [syncer] %s for %s/%s", msg, row.Name, row.SourceRef)
_ = s.queries.ApplySyncError(row.ID, msg)
return
return fmt.Errorf("%s", msg)
}
fetchCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
@ -199,7 +200,7 @@ func (s *Syncer) syncOne(ctx context.Context, row models.TrackedSoftware) {
if dbErr := s.queries.ApplySyncError(row.ID, err.Error()); dbErr != nil {
log.Printf("[ERROR] [upstream] [syncer] could not record fetch error: %v", dbErr)
}
return
return err
}
priorLatest := ""
@ -209,7 +210,7 @@ func (s *Syncer) syncOne(ctx context.Context, row models.TrackedSoftware) {
if err := s.queries.ApplySyncResult(row.ID, release.Version, release.PublishedAt, release.EOLAt); err != nil {
log.Printf("[ERROR] [upstream] [syncer] apply_sync_result failed: %v", err)
return
return err
}
// Emit drift event when latest_version actually moved. The cheap heuristic
@ -231,4 +232,5 @@ func (s *Syncer) syncOne(ctx context.Context, row models.TrackedSoftware) {
log.Printf("[INFO] [upstream] [syncer] drift name=%s severity=%s %s -> %s", row.Name, severity, from, release.Version)
}
}
return nil
}