mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-08-11 01:53:57 +00:00
Every list-shaped resources request deep-cloned the whole registry: HandleListResources via ListForPresentation, the storage summary and incidents handlers and the k8s namespaces handler via List/ListByType, and the stats handler once more. With the frontend polling three pages plus summaries every few seconds, the same unchanged world was cloned dozens of times between snapshot updates. Cache the raw and presentation lists on the existing per-generation registry cache entry (same invalidation: entries rebuild when the seed lastUpdate moves) and hand requests a flat top-level copy instead. Every decorator in the request pipeline was audited to write only top-level fields on request-owned elements: action availability, discovery targets and readiness, metrics targets, canonical metadata refresh, and contract types all assign freshly built values. The one nested writer — the PMG list prune, which cleared relay domains through the shared pointer — now clones the PMG struct before clearing, with a regression test pinning both the clone-on-write and the cache's immunity to request decoration. Read-only consumers (stats aggregation, storage filtering, namespace counting) use the shared list with no copy at all. Single-resource lookups (presentationResourceByReference and its callers) still deep-clone per lookup; they are cold paths and stay as-is. Contract-Neutral: per-generation shared resource lists: response bytes pinned by contract tests, no payload delta
123 lines
3.9 KiB
Go
123 lines
3.9 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
unifiedresources "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
)
|
|
|
|
func TestPruneResourceForListResponseDoesNotMutateSharedPMG(t *testing.T) {
|
|
sharedPMG := &unifiedresources.PMGData{
|
|
InstanceID: "pmg-1",
|
|
RelayDomains: []unifiedresources.PMGRelayDomainMeta{{Domain: "example.com"}},
|
|
DomainStatsAsOf: time.Date(2026, 8, 5, 12, 0, 0, 0, time.UTC),
|
|
}
|
|
resource := unifiedresources.Resource{ID: "pmg-resource", PMG: sharedPMG}
|
|
|
|
pruneResourceForListResponse(&resource)
|
|
|
|
if resource.PMG == sharedPMG {
|
|
t.Fatal("expected prune to clone the PMG struct before clearing it")
|
|
}
|
|
if resource.PMG.RelayDomains != nil || !resource.PMG.DomainStatsAsOf.IsZero() {
|
|
t.Fatal("expected the pruned copy to be cleared")
|
|
}
|
|
if len(sharedPMG.RelayDomains) != 1 || sharedPMG.DomainStatsAsOf.IsZero() {
|
|
t.Fatal("expected the shared PMG struct to remain untouched")
|
|
}
|
|
}
|
|
|
|
func TestSharedResourceListsCachedPerGenerationAndImmuneToRequestDecoration(t *testing.T) {
|
|
now := time.Date(2026, 8, 5, 12, 0, 0, 0, time.UTC)
|
|
h := newActionTestResourceHandlers(t, &config.Config{DataPath: t.TempDir()})
|
|
h.SetStateProvider(resourceUnifiedSeedProvider{
|
|
snapshot: models.StateSnapshot{LastUpdate: now},
|
|
resources: []unifiedresources.Resource{
|
|
{
|
|
ID: "vm-1",
|
|
Type: unifiedresources.ResourceTypeVM,
|
|
Name: "worker",
|
|
Status: unifiedresources.StatusOnline,
|
|
LastSeen: now,
|
|
Sources: []unifiedresources.DataSource{unifiedresources.SourceProxmox},
|
|
},
|
|
{
|
|
ID: "pmg-1",
|
|
Type: unifiedresources.ResourceTypePMG,
|
|
Name: "mailgw",
|
|
Status: unifiedresources.StatusOnline,
|
|
LastSeen: now,
|
|
Sources: []unifiedresources.DataSource{unifiedresources.SourceProxmox},
|
|
PMG: &unifiedresources.PMGData{
|
|
InstanceID: "pmg-1",
|
|
RelayDomains: []unifiedresources.PMGRelayDomainMeta{{Domain: "example.com"}},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
orgID := ""
|
|
first, _, err := h.sharedPresentationResources(orgID)
|
|
if err != nil {
|
|
t.Fatalf("sharedPresentationResources: %v", err)
|
|
}
|
|
if len(first) == 0 {
|
|
t.Fatal("expected seeded resources")
|
|
}
|
|
second, _, err := h.sharedPresentationResources(orgID)
|
|
if err != nil {
|
|
t.Fatalf("sharedPresentationResources: %v", err)
|
|
}
|
|
if &first[0] != &second[0] {
|
|
t.Fatal("expected the cached shared list for an unchanged registry generation")
|
|
}
|
|
|
|
// A full list request prunes PMG detail in its response; the shared cache
|
|
// must keep the untouched data for later consumers.
|
|
req := httptest.NewRequest(http.MethodGet, "/api/resources", nil)
|
|
req = req.WithContext(context.WithValue(req.Context(), OrgIDContextKey, orgID))
|
|
rec := httptest.NewRecorder()
|
|
h.HandleListResources(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
after, _, err := h.sharedPresentationResources(orgID)
|
|
if err != nil {
|
|
t.Fatalf("sharedPresentationResources: %v", err)
|
|
}
|
|
if &first[0] != &after[0] {
|
|
t.Fatal("expected the request to serve from the cached list, not rebuild it")
|
|
}
|
|
foundPMG := false
|
|
for i := range after {
|
|
if after[i].PMG == nil {
|
|
continue
|
|
}
|
|
foundPMG = true
|
|
if len(after[i].PMG.RelayDomains) != 1 {
|
|
t.Fatal("expected the shared cache to retain PMG relay domains after a list request pruned its response copy")
|
|
}
|
|
}
|
|
if !foundPMG {
|
|
t.Fatal("expected a PMG-bearing resource in the shared list")
|
|
}
|
|
|
|
rawFirst, _, err := h.sharedRawResources(orgID)
|
|
if err != nil {
|
|
t.Fatalf("sharedRawResources: %v", err)
|
|
}
|
|
rawSecond, _, err := h.sharedRawResources(orgID)
|
|
if err != nil {
|
|
t.Fatalf("sharedRawResources: %v", err)
|
|
}
|
|
if len(rawFirst) == 0 || &rawFirst[0] != &rawSecond[0] {
|
|
t.Fatal("expected the cached shared raw list for an unchanged registry generation")
|
|
}
|
|
}
|