forked from lei.chen/myxce
feat: P0-1e expand handleConfigPush with host config and cluster dispatch
Wires the remaining cluster_config_t data into the notification pipeline: - dispatchConfigPush() extracted as a pure-Go helper (CGO-free, testable) that drives OnQueueUpdated, OnResourceUpdated, OnHostConfigUpdated, and OnClusterUpdated from a single config push event - handleConfigPush() delegates to dispatchConfigPush via the new helper, eliminating the previous inline loops - OnHostConfigUpdated and OnClusterUpdated added to NotificationHandler interface and implemented with partial-merge semantics in DefaultHandler - rawHostConfigData/rawHostConfigToModel and rawClusterData/rawClusterToModel added to convert_pure.go with their CGO extraction counterparts in convert.go Safety fixes from code review: - OnHostUpdated, OnHostConfigUpdated, OnClusterUpdated now copy *incoming to a local merged value before mutation, closing the TOCTOU window between store.Get and store.Put under concurrent pushes - cClusterConfQueues and cClusterConfResources gain nil-pointer guards matching the existing guard in cClusterConfHostConfigs - handleConfigPush gains an explicit gHandler nil check - cClusterConfToClusterInfo returns nil on empty clustername (prevents phantom empty-key write to store.Clusters); handleConfigPush logs a warning when nil - 36 unit tests passing (CGO_ENABLED=0)
This commit is contained in:
parent
00c4472d6c
commit
893812e7aa
8 changed files with 850 additions and 11 deletions
|
|
@ -1,6 +1,7 @@
|
|||
package backend
|
||||
|
||||
import (
|
||||
"apiserver/models"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
|
@ -387,3 +388,200 @@ func TestIsArrayJob(t *testing.T) {
|
|||
t.Errorf("array child with idx=0 should still be an array job")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawHostConfigToModel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
raw rawHostConfigData
|
||||
want models.HostInfo
|
||||
}{
|
||||
{
|
||||
name: "normal values",
|
||||
raw: rawHostConfigData{
|
||||
name: "node01",
|
||||
hostmodel: "x86_64",
|
||||
hosttype: "LINUX64",
|
||||
mxj: 8,
|
||||
},
|
||||
want: models.HostInfo{
|
||||
HostName: "node01",
|
||||
HostModel: "x86_64",
|
||||
HostType: "LINUX64",
|
||||
MaxSlot: 8,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "zero values for integer fields",
|
||||
raw: rawHostConfigData{
|
||||
name: "node02",
|
||||
hostmodel: "modelB",
|
||||
hosttype: "linux",
|
||||
mxj: 0,
|
||||
},
|
||||
want: models.HostInfo{
|
||||
HostName: "node02",
|
||||
HostModel: "modelB",
|
||||
HostType: "linux",
|
||||
MaxSlot: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty strings for name/hostmodel/hosttype",
|
||||
raw: rawHostConfigData{
|
||||
name: "",
|
||||
hostmodel: "",
|
||||
hosttype: "",
|
||||
mxj: 4,
|
||||
},
|
||||
want: models.HostInfo{
|
||||
HostName: "",
|
||||
HostModel: "",
|
||||
HostType: "",
|
||||
MaxSlot: 4,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "worker-sourced fields remain zero",
|
||||
raw: rawHostConfigData{
|
||||
name: "node03",
|
||||
hostmodel: "modelC",
|
||||
hosttype: "CENTOS7",
|
||||
mxj: 16,
|
||||
},
|
||||
want: models.HostInfo{
|
||||
HostName: "node03",
|
||||
HostModel: "modelC",
|
||||
HostType: "CENTOS7",
|
||||
MaxSlot: 16,
|
||||
HostStatus: 0,
|
||||
CPUFactor: 0,
|
||||
MaxMem: 0,
|
||||
NumRun: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := rawHostConfigToModel(tt.raw)
|
||||
if got.HostName != tt.want.HostName {
|
||||
t.Errorf("HostName: got %q, want %q", got.HostName, tt.want.HostName)
|
||||
}
|
||||
if got.HostModel != tt.want.HostModel {
|
||||
t.Errorf("HostModel: got %q, want %q", got.HostModel, tt.want.HostModel)
|
||||
}
|
||||
if got.HostType != tt.want.HostType {
|
||||
t.Errorf("HostType: got %q, want %q", got.HostType, tt.want.HostType)
|
||||
}
|
||||
if got.MaxSlot != tt.want.MaxSlot {
|
||||
t.Errorf("MaxSlot: got %d, want %d", got.MaxSlot, tt.want.MaxSlot)
|
||||
}
|
||||
// Verify worker-sourced fields stay zero
|
||||
if got.HostStatus != tt.want.HostStatus {
|
||||
t.Errorf("HostStatus: got %d, want %d (should be zero, worker-sourced)", got.HostStatus, tt.want.HostStatus)
|
||||
}
|
||||
if got.CPUFactor != tt.want.CPUFactor {
|
||||
t.Errorf("CPUFactor: got %f, want %f (should be zero, worker-sourced)", got.CPUFactor, tt.want.CPUFactor)
|
||||
}
|
||||
if got.MaxMem != tt.want.MaxMem {
|
||||
t.Errorf("MaxMem: got %d, want %d (should be zero, worker-sourced)", got.MaxMem, tt.want.MaxMem)
|
||||
}
|
||||
if got.NumRun != tt.want.NumRun {
|
||||
t.Errorf("NumRun: got %d, want %d (should be zero, worker-sourced)", got.NumRun, tt.want.NumRun)
|
||||
}
|
||||
// DisptWindow is deferred — must be empty string, not accidentally set
|
||||
if got.DisptWindow != "" {
|
||||
t.Errorf("DisptWindow: got %q, want empty string (deferred field)", got.DisptWindow)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawClusterToModel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
raw rawClusterData
|
||||
want models.ClusterInfo
|
||||
}{
|
||||
{
|
||||
name: "normal values",
|
||||
raw: rawClusterData{
|
||||
name: "mycluster",
|
||||
heartbeatInterval: 30,
|
||||
vcpus: 16,
|
||||
inactiveHeartbeatCount: 3,
|
||||
},
|
||||
want: models.ClusterInfo{
|
||||
ClusterName: "mycluster",
|
||||
HeartbeatInterval: 30,
|
||||
Vcpus: 16,
|
||||
InactiveHeartbeatCount: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty cluster name maps through unchanged (guard is in CGO layer)",
|
||||
raw: rawClusterData{
|
||||
name: "",
|
||||
heartbeatInterval: 60,
|
||||
vcpus: 8,
|
||||
inactiveHeartbeatCount: 5,
|
||||
},
|
||||
want: models.ClusterInfo{
|
||||
ClusterName: "",
|
||||
HeartbeatInterval: 60,
|
||||
Vcpus: 8,
|
||||
InactiveHeartbeatCount: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "built-in fields remain zero",
|
||||
raw: rawClusterData{
|
||||
name: "test",
|
||||
heartbeatInterval: 45,
|
||||
vcpus: 32,
|
||||
inactiveHeartbeatCount: 2,
|
||||
},
|
||||
want: models.ClusterInfo{
|
||||
ClusterName: "test",
|
||||
HeartbeatInterval: 45,
|
||||
Vcpus: 32,
|
||||
InactiveHeartbeatCount: 2,
|
||||
Version: "",
|
||||
Status: 0,
|
||||
ControllerName: "",
|
||||
NumServers: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := rawClusterToModel(tt.raw)
|
||||
if got.ClusterName != tt.want.ClusterName {
|
||||
t.Errorf("ClusterName: got %q, want %q", got.ClusterName, tt.want.ClusterName)
|
||||
}
|
||||
if got.HeartbeatInterval != tt.want.HeartbeatInterval {
|
||||
t.Errorf("HeartbeatInterval: got %d, want %d", got.HeartbeatInterval, tt.want.HeartbeatInterval)
|
||||
}
|
||||
if got.Vcpus != tt.want.Vcpus {
|
||||
t.Errorf("Vcpus: got %d, want %d", got.Vcpus, tt.want.Vcpus)
|
||||
}
|
||||
if got.InactiveHeartbeatCount != tt.want.InactiveHeartbeatCount {
|
||||
t.Errorf("InactiveHeartbeatCount: got %d, want %d", got.InactiveHeartbeatCount, tt.want.InactiveHeartbeatCount)
|
||||
}
|
||||
// Verify built-in/sourced fields stay zero (not overwritten by config push)
|
||||
if got.Version != tt.want.Version {
|
||||
t.Errorf("Version: got %q, want %q (should be empty, built-in-sourced)", got.Version, tt.want.Version)
|
||||
}
|
||||
if got.Status != tt.want.Status {
|
||||
t.Errorf("Status: got %d, want %d (should be zero, built-in-sourced)", got.Status, tt.want.Status)
|
||||
}
|
||||
if got.ControllerName != tt.want.ControllerName {
|
||||
t.Errorf("ControllerName: got %q, want %q (should be empty, built-in-sourced)", got.ControllerName, tt.want.ControllerName)
|
||||
}
|
||||
if got.NumServers != tt.want.NumServers {
|
||||
t.Errorf("NumServers: got %d, want %d (should be zero, built-in-sourced)", got.NumServers, tt.want.NumServers)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue