Preserve independent ARC and stop cancelled CORE reporting before I/O

Complete the native CORE #2077 repair after its whole-suite adverse result: retain ARC without inventing free RAM, enforce live-window freshness even with coarse RRD steps, and assert bounded graph splitting including CPU temperature. Keep empty/zero, aligned ARC and transport boundaries intact.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-02 01:49:25 +01:00
parent bab5cf77d6
commit 579eee2907
5 changed files with 45 additions and 7 deletions
@@ -291,13 +291,16 @@ row timestamps and supported object aliases retain compatibility. A metric is
never a timestamp, a null is never zero and a window mean is never a replacement
for a missing current row. History retains measured buckets; native current
readings accept only the last two steps before the requested end, preserving
sample time rather than stamping an old value as newly measured. The five-state
sample time rather than stamping an old value as newly measured. This allowance
never exceeds the live query window, even with a coarse returned step. The five-state
CORE CPU vector is normalized by its sum (RRD state rates need not sum to 100).
All-zero CPU or complete memory-class vectors are empty buckets, not 100% usage;
explicit zero usage, all-idle CPU, zero-free RAM with used pages and zero I/O
remain measured zeros. Free memory comes from `memory-free_value`, not active
pages or the sum of memory classes. ARC subtraction uses the matching free-RAM
bucket. Reporting I/O values remain rates in the existing bytes/s contract;
bucket when free RAM is present; independently reported ARC remains available
without claiming measured memory usage. Pre-cancelled collection stops before
reading the catalogue or posting a reporting query. Reporting I/O values remain rates in the existing bytes/s contract;
step supplies time, not a second rate division; `overlap` is not extra traffic.
Legacy REST reads `/reporting/graphs` once per live/History collection and sends
+8
View File
@@ -342,6 +342,14 @@ func systemInfoFromMetricHistory(history *SystemMetricHistory) *SystemInfo {
latestFree = free.Timestamp
}
}
// ARC can be observed independently when no free-RAM sample exists;
// retain it without implying known memory usage. With free RAM, require
// cache and free to describe the same bucket before deriving usage.
if latestFree.IsZero() {
if value, ok := latestTimeSeriesValue(history.ARCSizeBytes); ok {
system.ARCSizeBytes = int64(value)
}
}
for _, arc := range history.ARCSizeBytes {
if arc.Timestamp.Equal(latestFree) {
system.ARCSizeBytes = int64(arc.Value)
+3 -3
View File
@@ -2195,8 +2195,8 @@ func TestRESTReportingGraphFailurePreservesSnapshotTelemetry(t *testing.T) {
t.Fatal("inventory lost")
}
}
if transport.calls != 12 {
t.Fatalf("reporting calls = %d, want bounded batch + five graphs per snapshot", transport.calls)
if transport.calls != 2*(1+len(legacyRESTReportingGraphs())) {
t.Fatalf("reporting calls = %d, want bounded batch + selected graphs per snapshot", transport.calls)
}
history, err := client.GetSystemMetricHistory(context.Background(), time.Hour)
if err != nil {
@@ -2464,7 +2464,7 @@ func TestRESTSnapshotRetainsSanitizedTelemetryFailure(t *testing.T) {
}
wantCalls := 1
if tc.status == 422 {
wantCalls = 6
wantCalls = 1 + len(legacyRESTReportingGraphs())
}
if transport.calls != wantCalls {
t.Fatalf("reporting calls=%d, want %d", transport.calls, wantCalls)
+7 -2
View File
@@ -16,6 +16,9 @@ import (
// Older REST bridges without the catalogue retain the unscoped compatibility
// request. Authentication, transport and malformed replies are never retried.
func (c *Client) legacyReportingGraphs(ctx context.Context) ([]map[string]any, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
var catalogue []struct {
Name string `json:"name"`
Identifiers []string `json:"identifiers"`
@@ -97,7 +100,9 @@ func reportingRow(response trueNASReportingGetDataResponse, index int) (time.Tim
// Keep native History intact, but do not label an old RRD point as a current
// reading. Two returned steps allow the usual unfinished/null RRD bucket;
// older points stay available only through History. Preserve row indices.
// older points stay available only through History. Never exceed the live
// query window even if a coarse/malformed step allows older buckets. Preserve
// row indices.
func liveReportingResponses(responses []trueNASReportingGetDataResponse, end int64) []trueNASReportingGetDataResponse {
live := append([]trueNASReportingGetDataResponse(nil), responses...)
for i, response := range live {
@@ -108,7 +113,7 @@ func liveReportingResponses(responses []trueNASReportingGetDataResponse, end int
for index := range response.Data {
timestamp, _, ok := reportingRow(response, index)
gap := end - timestamp.Unix()
if !ok || timestamp.Unix() > end || (gap > response.Step && gap-response.Step > response.Step) {
if !ok || timestamp.Unix() > end || gap > legacyRESTTelemetryWindowSeconds || (gap > response.Step && gap-response.Step > response.Step) {
live[i].Data[index] = nil
}
}
+22
View File
@@ -417,6 +417,28 @@ func TestCORE13ARCAlignedWithFreeMemory(t *testing.T) {
assertCOREPoint(t, points, 0, 1789000000, 0)
}
func TestCORE13CoarseStepDoesNotMakeOldRowsCurrent(t *testing.T) {
response := trueNASReportingGetDataResponse{Name: "cpu", Legend: []string{"usage"}, Start: 1789000000, End: 1789000000, Step: 3600, Data: []any{[]any{12.}}}
if history := parseSystemMetricHistory(liveReportingResponses([]trueNASReportingGetDataResponse{response}, 1789000400)); history != nil {
t.Fatal("coarse step made an old CPU sample current")
}
if history := parseSystemMetricHistory([]trueNASReportingGetDataResponse{response}); history == nil {
t.Fatal("live age filtering destroyed native History")
}
}
func TestCORE13ARCOnlyDoesNotInventMemoryUsage(t *testing.T) {
history := &SystemMetricHistory{ARCSizeBytes: []TimeSeriesPoint{{Timestamp: time.Unix(1789000000, 0), Value: 8 << 30}}}
system := systemInfoFromMetricHistory(history)
if system.ARCSizeBytes != 8<<30 || system.Telemetry.Memory {
t.Fatal("ARC-only telemetry lost or claimed free RAM")
}
system.MemoryTotalBytes = 32 << 30
if metricsFromTrueNASSystem(*system, 0, 0).Memory != nil {
t.Fatal("ARC-only reading invented usage")
}
}
func TestCORE13DiskTemperatureHistoryExternalTiming(t *testing.T) {
response := trueNASReportingGetDataResponse{Name: "disktemp", Identifier: "disk-a", Legend: []string{"temperature"}, Start: 1789000000, End: 1789000020, Step: 10, Data: []any{[]any{40.}, []any{nil}, []any{42.}}}
points := parseReportingDiskTemperatureHistory([]trueNASReportingGetDataResponse{response})["disk-a"]