From 6dced22499784e46299ea9bb93c38aca68e16116 Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Thu, 6 Aug 2026 16:20:34 +0100 Subject: [PATCH] fix: Fixed agent collect packages --- agent/internal/sync/packages.go | 35 +++++++++++++++++++++++++++++++++ agent/internal/sync/sync.go | 5 +++++ 2 files changed, 40 insertions(+) diff --git a/agent/internal/sync/packages.go b/agent/internal/sync/packages.go index 6ebb316..4830d22 100644 --- a/agent/internal/sync/packages.go +++ b/agent/internal/sync/packages.go @@ -1,9 +1,12 @@ package agentsync import ( + "context" "log" "runtime" + "sync" "sync/atomic" + "time" "gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/config" grpcclient "gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/grpc" @@ -20,6 +23,38 @@ import ( // are not paying for. var collectPackagesFlag atomic.Bool +// firstPoll closes once a SyncKeys response has set the flag above. +// +// Without it the boot-time package report loses a race it can only lose: the +// hourly loop starts before the first poll, reads a flag that is still false by +// construction, and skips — so a freshly installed agent reports no packages for +// an hour and the server shows nothing to scan. +// How long the boot package report waits for that first poll. Two poll +// intervals plus slack: long enough to cover one failed attempt, short enough +// that a dead control plane does not hold the OS-update report hostage. +const firstPollWait = 90 * time.Second + +var ( + firstPoll = make(chan struct{}) + firstPollOnce sync.Once +) + +func markFirstPoll() { firstPollOnce.Do(func() { close(firstPoll) }) } + +// waitFirstPoll blocks until the flag is known, or gives up. The wait is +// bounded because this loop also reports OS updates, which do not depend on the +// flag at all — a control plane that cannot be polled must not silence those too. +func waitFirstPoll(ctx context.Context, limit time.Duration) { + t := time.NewTimer(limit) + defer t.Stop() + select { + case <-firstPoll: + case <-t.C: + log.Printf("package collection: no SyncKeys response within %s, collecting nothing this round", limit) + case <-ctx.Done(): + } +} + func collectPackagesEnabled() bool { return collectPackagesFlag.Load() } // reportPackages offers a hash of the installed package set and sends the full diff --git a/agent/internal/sync/sync.go b/agent/internal/sync/sync.go index 538c008..ba0bd1e 100644 --- a/agent/internal/sync/sync.go +++ b/agent/internal/sync/sync.go @@ -101,6 +101,7 @@ func poll(client *grpcclient.Client, cfg *config.Config, version string) error { // goroutine. Absent on the wire decodes as false, so an older server leaves // collection off rather than on. collectPackagesFlag.Store(resp.CollectPackages) + markFirstPoll() desired := resp.PublicKeys @@ -409,6 +410,10 @@ func runUpdateCheck(ctx context.Context, cfg *config.Config) { reportPackages(client, cfg) } + // The boot round only: after this the flag has long been set, and every + // later tick is an hour past a poll that runs every 30s. + waitFirstPoll(ctx, firstPollWait) + doCheck() ticker := time.NewTicker(interval) defer ticker.Stop()