// Copyright (C) NHR@FAU, University Erlangen-Nuremberg. // All rights reserved. This file is part of cc-backend. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package metricstore import ( "slices" "sync" "testing" "time" "github.com/ClusterCockpit/cc-lib/v2/schema" ) // writeNode creates cluster/node (and optional deeper sub-levels) and fills // every metric buffer with points from startTs to endTs (inclusive) at the // store's metric frequency. subPath appends levels below the node (e.g. // {"socket0"}); pass nil to write buffers directly on the node level. func writeNode(ms *MemoryStore, cluster, node string, subPath []string, freq, startTs, endTs int64) { selector := append([]string{cluster, node}, subPath...) lvl := ms.root.findLevelOrCreate(selector, len(ms.Metrics)) for i := range lvl.metrics { lvl.metrics[i] = newBuffer(startTs, freq) for ts := startTs; ts <= endTs; ts += freq { lvl.metrics[i].write(ts, schema.Float(1)) } } } // hasNode reports whether [cluster, node] still exists in the tree. func hasNode(ms *MemoryStore, cluster, node string) bool { return slices.Contains(ms.ListChildren([]string{cluster}), node) } func TestFreeExcludingUsedPrunesDeadNode(t *testing.T) { ms := newTestStore() // one metric "cpu_load", freq 60 const freq, thr = int64(60), int64(100000) // dead: last data ~2000, well below threshold -> emptied -> pruned writeNode(ms, "fritz", "dead", nil, freq, 1000, 2000) // alive: data around/after threshold -> stays writeNode(ms, "fritz", "alive", nil, freq, thr, thr+600) freed, err := ms.root.freeExcludingUsed(thr, nil) if err != nil { t.Fatalf("freeExcludingUsed: %v", err) } if freed == 0 { t.Fatal("expected at least one buffer freed") } if hasNode(ms, "fritz", "dead") { t.Error("dead node must be pruned from the tree") } if !hasNode(ms, "fritz", "alive") { t.Error("alive node must be preserved") } } func TestFreeExcludingUsedPreservesUsedNode(t *testing.T) { ms := newTestStore() const freq, thr = int64(60), int64(100000) // both would be dead by timestamp, but "used" is excluded writeNode(ms, "fritz", "used", nil, freq, 1000, 2000) writeNode(ms, "fritz", "gone", nil, freq, 1000, 2000) used := map[string][]string{"fritz": {"used"}} // sorted hostnames if _, err := ms.root.freeExcludingUsed(thr, used); err != nil { t.Fatalf("freeExcludingUsed: %v", err) } if !hasNode(ms, "fritz", "used") { t.Error("used node must be preserved even when stale") } if hasNode(ms, "fritz", "gone") { t.Error("non-used dead node must be pruned") } // used node still holds its buffer lvl := ms.root.findLevel([]string{"fritz", "used"}) if lvl == nil || lvl.metrics[0] == nil { t.Error("used node must keep its buffers") } } func TestFreeExcludingUsedPrunesSubLevelNode(t *testing.T) { ms := newTestStore() const freq, thr = int64(60), int64(100000) // node holds no direct buffers; only a socket0 child, all stale writeNode(ms, "fritz", "deep", []string{"socket0"}, freq, 1000, 2000) if _, err := ms.root.freeExcludingUsed(thr, nil); err != nil { t.Fatalf("freeExcludingUsed: %v", err) } if hasNode(ms, "fritz", "deep") { t.Error("node must be pruned once all descendant buffers are freed") } } func TestFreeViaProviderPrunesDeadNodes(t *testing.T) { ms := newTestStore() const freq int64 = 60 thr := time.Unix(100000, 0) writeNode(ms, "fritz", "dead", nil, freq, 1000, 2000) // stale, not used -> pruned writeNode(ms, "fritz", "busy", nil, freq, 1000, 2000) // stale, but used -> kept writeNode(ms, "fritz", "live", nil, freq, 100000, 100600) // fresh -> kept ms.SetNodeProvider(&fakeNodeProvider{nodes: map[string][]string{"fritz": {"busy"}}}) if _, err := Free(ms, thr); err != nil { t.Fatalf("Free: %v", err) } if hasNode(ms, "fritz", "dead") { t.Error("stale non-used node must be pruned") } if !hasNode(ms, "fritz", "busy") { t.Error("used node must survive") } if !hasNode(ms, "fritz", "live") { t.Error("fresh node must survive") } } func TestFreeExcludingUsedConcurrentReadNoRace(t *testing.T) { ms := newTestStore() const freq, thr = int64(60), int64(100000) for _, n := range []string{"a", "b", "c", "d"} { writeNode(ms, "fritz", n, nil, freq, 1000, 2000) } var wg sync.WaitGroup stop := make(chan struct{}) for i := 0; i < 4; i++ { wg.Add(1) go func() { defer wg.Done() for { select { case <-stop: return default: _ = ms.ListChildren([]string{"fritz"}) } } }() } if _, err := ms.root.freeExcludingUsed(thr, nil); err != nil { t.Fatalf("freeExcludingUsed: %v", err) } close(stop) wg.Wait() }