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:
parent
32c195ef2f
commit
413211c657
4 changed files with 19 additions and 11 deletions
|
|
@ -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}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue