Adopt config to use policy based resampler configuration

Entire-Checkpoint: 7536f551d548
This commit is contained in:
2026-03-20 08:03:34 +01:00
parent 0069c86e81
commit 0c56591e4b
11 changed files with 139 additions and 112 deletions

View File

@@ -106,12 +106,10 @@ type NodeStateRetention struct {
}
type ResampleConfig struct {
// Minimum number of points to trigger resampling of data
MinimumPoints int `json:"minimum-points"`
// Array of resampling target resolutions, in seconds; Example: [600,300,60]
Resolutions []int `json:"resolutions"`
// Trigger next zoom level at less than this many visible datapoints
Trigger int `json:"trigger"`
// Default resample policy when no user preference is set ("low", "medium", "high")
DefaultPolicy string `json:"default-policy"`
// Default resample algorithm when no user preference is set ("lttb", "average", "simple")
DefaultAlgo string `json:"default-algo"`
// Policy-derived target point count (set dynamically from user preference, not from config.json)
TargetPoints int `json:"targetPoints,omitempty"`
}
@@ -157,7 +155,24 @@ func Init(mainConfig json.RawMessage) {
cclog.Abortf("Config Init: Could not decode config file '%s'.\nError: %s\n", mainConfig, err.Error())
}
if Keys.EnableResampling != nil && Keys.EnableResampling.MinimumPoints > 0 {
resampler.SetMinimumRequiredPoints(Keys.EnableResampling.MinimumPoints)
if Keys.EnableResampling != nil {
policy := Keys.EnableResampling.DefaultPolicy
if policy == "" {
policy = "medium"
}
resampler.SetMinimumRequiredPoints(targetPointsForPolicy(policy))
}
}
func targetPointsForPolicy(policy string) int {
switch policy {
case "low":
return 200
case "medium":
return 500
case "high":
return 1000
default:
return 500
}
}

View File

@@ -92,23 +92,17 @@ var configSchema = `
"description": "Enable dynamic zoom in frontend metric plots.",
"type": "object",
"properties": {
"minimum-points": {
"description": "Minimum points to trigger resampling of time-series data.",
"type": "integer"
"default-policy": {
"description": "Default resample policy when no user preference is set.",
"type": "string",
"enum": ["low", "medium", "high"]
},
"trigger": {
"description": "Trigger next zoom level at less than this many visible datapoints.",
"type": "integer"
},
"resolutions": {
"description": "Array of resampling target resolutions, in seconds.",
"type": "array",
"items": {
"type": "integer"
}
"default-algo": {
"description": "Default resample algorithm when no user preference is set.",
"type": "string",
"enum": ["lttb", "average", "simple"]
}
},
"required": ["trigger", "resolutions"]
}
},
"api-subjects": {
"description": "NATS subjects configuration for subscribing to job and node events.",

View File

@@ -8,6 +8,7 @@ import (
"context"
"strings"
"github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/internal/graph/model"
"github.com/ClusterCockpit/cc-backend/internal/metricdispatch"
"github.com/ClusterCockpit/cc-backend/internal/repository"
@@ -71,15 +72,46 @@ func resolveResampleAlgo(ctx context.Context, resampleAlgo *model.ResampleAlgo)
}
algoVal, ok := conf["plotConfiguration_resampleAlgo"]
if !ok {
return ""
}
algoStr, ok := algoVal.(string)
if !ok {
return ""
if ok {
if algoStr, ok := algoVal.(string); ok && algoStr != "" {
return algoStr
}
}
return algoStr
// Fall back to global default algo
if config.Keys.EnableResampling != nil && config.Keys.EnableResampling.DefaultAlgo != "" {
return config.Keys.EnableResampling.DefaultAlgo
}
return ""
}
// resolveResolutionFromDefaultPolicy computes a resolution using the global
// default policy from config. Returns nil if no policy is configured.
func resolveResolutionFromDefaultPolicy(duration int64, cluster string, metrics []string) *int {
cfg := config.Keys.EnableResampling
if cfg == nil {
return nil
}
policyStr := cfg.DefaultPolicy
if policyStr == "" {
policyStr = "medium"
}
policy := metricdispatch.ResamplePolicy(policyStr)
targetPoints := metricdispatch.TargetPointsForPolicy(policy)
if targetPoints == 0 {
return nil
}
frequency := smallestFrequency(cluster, metrics)
if frequency <= 0 {
return nil
}
res := metricdispatch.ComputeResolution(duration, int64(frequency), targetPoints)
return &res
}
// smallestFrequency returns the smallest metric timestep (in seconds) among the

View File

@@ -511,9 +511,9 @@ func (r *queryResolver) JobMetrics(ctx context.Context, id string, metrics []str
}
if resolution == nil {
if config.Keys.EnableResampling != nil {
defaultRes := slices.Max(config.Keys.EnableResampling.Resolutions)
resolution = &defaultRes
} else {
resolution = resolveResolutionFromDefaultPolicy(int64(job.Duration), job.Cluster, metrics)
}
if resolution == nil {
defaultRes := 0
resolution = &defaultRes
}
@@ -886,9 +886,9 @@ func (r *queryResolver) NodeMetricsList(ctx context.Context, cluster string, sub
}
if resolution == nil {
if config.Keys.EnableResampling != nil {
defaultRes := slices.Max(config.Keys.EnableResampling.Resolutions)
resolution = &defaultRes
} else {
resolution = resolveResolutionFromDefaultPolicy(duration, cluster, metrics)
}
if resolution == nil {
defaultRes := 0
resolution = &defaultRes
}

View File

@@ -597,13 +597,19 @@ func HandleSearchBar(rw http.ResponseWriter, r *http.Request, buildInfo web.Buil
func resamplingForUser(conf map[string]any) *config.ResampleConfig {
globalCfg := config.Keys.EnableResampling
policyVal, ok := conf["plotConfiguration_resamplePolicy"]
if !ok {
return globalCfg
policyStr := ""
if policyVal, ok := conf["plotConfiguration_resamplePolicy"]; ok {
if s, ok := policyVal.(string); ok {
policyStr = s
}
}
policyStr, ok := policyVal.(string)
if !ok || policyStr == "" {
return globalCfg
// Fall back to global default policy, then to "medium"
if policyStr == "" && globalCfg != nil {
policyStr = globalCfg.DefaultPolicy
}
if policyStr == "" {
policyStr = "medium"
}
policy := metricdispatch.ResamplePolicy(policyStr)
@@ -612,9 +618,7 @@ func resamplingForUser(conf map[string]any) *config.ResampleConfig {
return globalCfg
}
// Build a policy-derived config: targetPoints + trigger, no resolutions array
return &config.ResampleConfig{
TargetPoints: targetPoints,
Trigger: targetPoints / 4,
}
}