From ec4ee724de90bcd42c1951cddd8cca2d474d3e0f Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:47:50 +0100 Subject: [PATCH] Reset obsolete guest metadata scheduling on invalidation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The broad guest lifecycle proof caught a recovery regression when a known unavailable agent deliberately invalidated its cache. Clear that key’s obsolete refresh/backoff deadline without clearing active fetch ownership, independent installations or Proxmox uncertainty. Retain immediate available-poll recovery and early-failure backoff as distinct paths. Contract-Neutral: Preserve existing recovery on deliberate guest metadata cache invalidation; no guest lock, uncertainty, command, public API, configuration or duration changes. Change-source: pulse-maintainer --- .../v6/internal/subsystems/monitoring.md | 5 +++- internal/monitoring/guest_metadata.go | 6 ++++ .../guest_metadata_reservation_test.go | 29 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/docs/release-control/v6/internal/subsystems/monitoring.md b/docs/release-control/v6/internal/subsystems/monitoring.md index 6aef8103c9..a4d0beefad 100644 --- a/docs/release-control/v6/internal/subsystems/monitoring.md +++ b/docs/release-control/v6/internal/subsystems/monitoring.md @@ -558,7 +558,10 @@ completed. A cold cache cannot override another owner's reservation or an early failure's backoff. A denied reservation returns only previously retained identity (or none), without creating fresh evidence, a transport deferral or a new retry deadline. Other keys remain independent; completion releases only -in-flight ownership and preserves normal refresh/backoff scheduling. +in-flight ownership and preserves normal refresh/backoff scheduling. Deliberate +cache invalidation on agent unavailability also discards its obsolete scheduling +deadline, so a later available poll can begin a fresh metadata lifecycle; it +never discards an outstanding fetch owner or the shared Proxmox command fence. `guest_metadata_reservation_test.go` exercises real HTTP enrichment with cold and retained caches, active and expired scheduling holds, early unverified-lock diff --git a/internal/monitoring/guest_metadata.go b/internal/monitoring/guest_metadata.go index 250526feee..32f36c29d6 100644 --- a/internal/monitoring/guest_metadata.go +++ b/internal/monitoring/guest_metadata.go @@ -482,6 +482,12 @@ func (m *Monitor) clearGuestMetadataCache(instanceName, nodeName string, vmid in delete(m.guestMetadataCache, key) } m.guestMetadataMu.Unlock() + // Deliberate invalidation when the agent is unavailable starts a new + // metadata lifecycle. Do not carry its obsolete refresh/backoff deadline + // into a later available poll, or discard an outstanding fetch's ownership. + m.guestMetadataLimiterMu.Lock() + delete(m.guestMetadataLimiter, key) + m.guestMetadataLimiterMu.Unlock() } func cloneStringSlice(src []string) []string { diff --git a/internal/monitoring/guest_metadata_reservation_test.go b/internal/monitoring/guest_metadata_reservation_test.go index 832f6a549e..f1e55211b8 100644 --- a/internal/monitoring/guest_metadata_reservation_test.go +++ b/internal/monitoring/guest_metadata_reservation_test.go @@ -203,3 +203,32 @@ func TestGuestMetadataColdCacheHonoursEarlyBackoff(t *testing.T) { }) } } + +func TestGuestMetadataInvalidationKeepsActiveOwnership(t *testing.T) { + key := guestMetadataCacheKey("reservation", "node", 105) + other := guestMetadataCacheKey("independent", "node", 105) + future := time.Now().Add(time.Hour) + m := &Monitor{guestMetadataCache: map[string]guestMetadataCacheEntry{key: {osName: "old"}, other: {osName: "independent"}}, guestMetadataLimiter: map[string]time.Time{other: future}} + if !m.tryReserveGuestMetadataFetch(key, time.Now()) { + t.Fatal("owner could not reserve metadata") + } + m.clearGuestMetadataCache("reservation", "node", 105) + if _, exists := m.guestMetadataCache[key]; exists { + t.Error("invalidated metadata was retained") + } + if _, exists := m.guestMetadataLimiter[key]; exists { + t.Error("obsolete scheduling deadline survived invalidation") + } + if m.tryReserveGuestMetadataFetch(key, time.Now()) { + t.Fatal("invalidation admitted another in-flight metadata writer") + } + if m.guestMetadataCache[other].osName != "independent" || !m.guestMetadataLimiter[other].Equal(future) { + t.Error("invalidation crossed installation boundaries") + } + m.releaseGuestMetadataFetch(key) + assertMetadataReservationReleased(t, m) + if !m.tryReserveGuestMetadataFetch(key, time.Now()) { + t.Error("invalidated metadata did not allow a fresh lifecycle after completion") + } + m.releaseGuestMetadataFetch(key) +}