mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-26 05:29:45 +00:00
Reset obsolete guest metadata scheduling on invalidation
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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user