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
345
src/frontend/apiserver/backend/handler_test.go
Normal file
345
src/frontend/apiserver/backend/handler_test.go
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
package backend
|
||||
|
||||
import (
|
||||
"apiserver/models"
|
||||
"apiserver/store"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// resetStores reinitializes all global stores for test isolation.
|
||||
func resetStores() {
|
||||
store.InitStores()
|
||||
}
|
||||
|
||||
func TestOnHostConfigUpdated_NewHost(t *testing.T) {
|
||||
resetStores()
|
||||
h := DefaultHandler{}
|
||||
|
||||
host := &models.HostInfo{
|
||||
HostName: "node01",
|
||||
HostModel: "x86_64",
|
||||
HostType: "LINUX64",
|
||||
MaxSlot: 8,
|
||||
}
|
||||
|
||||
h.OnHostConfigUpdated(host)
|
||||
|
||||
got, ok := store.Hosts.Get("node01")
|
||||
if !ok {
|
||||
t.Fatal("host node01 should exist in store after OnHostConfigUpdated")
|
||||
}
|
||||
if got.HostModel != "x86_64" {
|
||||
t.Errorf("HostModel: got %q, want %q", got.HostModel, "x86_64")
|
||||
}
|
||||
if got.HostType != "LINUX64" {
|
||||
t.Errorf("HostType: got %q, want %q", got.HostType, "LINUX64")
|
||||
}
|
||||
if got.MaxSlot != 8 {
|
||||
t.Errorf("MaxSlot: got %d, want 8", got.MaxSlot)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnHostConfigUpdated_PartialMerge(t *testing.T) {
|
||||
resetStores()
|
||||
h := DefaultHandler{}
|
||||
|
||||
// Simulate a prior worker push that set worker-sourced fields.
|
||||
store.Hosts.Put("node01", models.HostInfo{
|
||||
HostName: "node01",
|
||||
HostStatus: 0x00010000, // UNAVAIL
|
||||
CPUFactor: 2.5,
|
||||
NumCPUs: 32,
|
||||
MaxMem: 131072,
|
||||
NumRun: 5,
|
||||
})
|
||||
|
||||
// Config push arrives — should merge config fields, preserve worker fields.
|
||||
h.OnHostConfigUpdated(&models.HostInfo{
|
||||
HostName: "node01",
|
||||
HostModel: "modelA",
|
||||
HostType: "CENTOS7",
|
||||
MaxSlot: 16,
|
||||
})
|
||||
|
||||
got, ok := store.Hosts.Get("node01")
|
||||
if !ok {
|
||||
t.Fatal("host node01 should exist after merge")
|
||||
}
|
||||
// Config-sourced fields taken from the push.
|
||||
if got.HostModel != "modelA" {
|
||||
t.Errorf("HostModel: got %q, want %q", got.HostModel, "modelA")
|
||||
}
|
||||
if got.HostType != "CENTOS7" {
|
||||
t.Errorf("HostType: got %q, want %q", got.HostType, "CENTOS7")
|
||||
}
|
||||
if got.MaxSlot != 16 {
|
||||
t.Errorf("MaxSlot: got %d, want 16", got.MaxSlot)
|
||||
}
|
||||
// Worker-sourced fields preserved from the prior entry.
|
||||
if got.HostStatus != 0x00010000 {
|
||||
t.Errorf("HostStatus: got 0x%08X, want 0x00010000 (should be preserved)", got.HostStatus)
|
||||
}
|
||||
if got.CPUFactor != 2.5 {
|
||||
t.Errorf("CPUFactor: got %f, want 2.5 (should be preserved)", got.CPUFactor)
|
||||
}
|
||||
if got.NumCPUs != 32 {
|
||||
t.Errorf("NumCPUs: got %d, want 32 (should be preserved)", got.NumCPUs)
|
||||
}
|
||||
if got.MaxMem != 131072 {
|
||||
t.Errorf("MaxMem: got %d, want 131072 (should be preserved)", got.MaxMem)
|
||||
}
|
||||
if got.NumRun != 5 {
|
||||
t.Errorf("NumRun: got %d, want 5 (should be preserved)", got.NumRun)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnClusterUpdated_NewCluster(t *testing.T) {
|
||||
resetStores()
|
||||
h := DefaultHandler{}
|
||||
|
||||
cluster := &models.ClusterInfo{
|
||||
ClusterName: "mycluster",
|
||||
HeartbeatInterval: 30,
|
||||
Vcpus: 16,
|
||||
InactiveHeartbeatCount: 3,
|
||||
}
|
||||
|
||||
h.OnClusterUpdated(cluster)
|
||||
|
||||
got, ok := store.Clusters.Get("mycluster")
|
||||
if !ok {
|
||||
t.Fatal("cluster mycluster should exist in store after OnClusterUpdated")
|
||||
}
|
||||
if got.HeartbeatInterval != 30 {
|
||||
t.Errorf("HeartbeatInterval: got %d, want 30", got.HeartbeatInterval)
|
||||
}
|
||||
if got.Vcpus != 16 {
|
||||
t.Errorf("Vcpus: got %d, want 16", got.Vcpus)
|
||||
}
|
||||
if got.InactiveHeartbeatCount != 3 {
|
||||
t.Errorf("InactiveHeartbeatCount: got %d, want 3", got.InactiveHeartbeatCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnClusterUpdated_PartialMerge(t *testing.T) {
|
||||
resetStores()
|
||||
h := DefaultHandler{}
|
||||
|
||||
// Simulate built-in data loaded at startup.
|
||||
store.Clusters.Put("mycluster", models.ClusterInfo{
|
||||
ClusterName: "mycluster",
|
||||
Version: "10.1.0",
|
||||
Status: 0x01, // CONNECTED
|
||||
ControllerName: "ctrl01",
|
||||
NumServers: 3,
|
||||
})
|
||||
|
||||
// Config push arrives — should merge config fields, preserve built-in fields.
|
||||
h.OnClusterUpdated(&models.ClusterInfo{
|
||||
ClusterName: "mycluster",
|
||||
HeartbeatInterval: 45,
|
||||
Vcpus: 64,
|
||||
InactiveHeartbeatCount: 5,
|
||||
})
|
||||
|
||||
got, ok := store.Clusters.Get("mycluster")
|
||||
if !ok {
|
||||
t.Fatal("cluster mycluster should exist after merge")
|
||||
}
|
||||
// Config-sourced fields from the push.
|
||||
if got.HeartbeatInterval != 45 {
|
||||
t.Errorf("HeartbeatInterval: got %d, want 45", got.HeartbeatInterval)
|
||||
}
|
||||
if got.Vcpus != 64 {
|
||||
t.Errorf("Vcpus: got %d, want 64", got.Vcpus)
|
||||
}
|
||||
if got.InactiveHeartbeatCount != 5 {
|
||||
t.Errorf("InactiveHeartbeatCount: got %d, want 5", got.InactiveHeartbeatCount)
|
||||
}
|
||||
// Built-in fields preserved.
|
||||
if got.Version != "10.1.0" {
|
||||
t.Errorf("Version: got %q, want %q (should be preserved)", got.Version, "10.1.0")
|
||||
}
|
||||
if got.Status != 0x01 {
|
||||
t.Errorf("Status: got 0x%02X, want 0x01 (should be preserved)", got.Status)
|
||||
}
|
||||
if got.ControllerName != "ctrl01" {
|
||||
t.Errorf("ControllerName: got %q, want %q (should be preserved)", got.ControllerName, "ctrl01")
|
||||
}
|
||||
if got.NumServers != 3 {
|
||||
t.Errorf("NumServers: got %d, want 3 (should be preserved)", got.NumServers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnHostUpdated_PartialMerge(t *testing.T) {
|
||||
resetStores()
|
||||
h := DefaultHandler{}
|
||||
|
||||
// Simulate a prior config push that set config-sourced fields.
|
||||
store.Hosts.Put("node01", models.HostInfo{
|
||||
HostName: "node01",
|
||||
HostModel: "x86_64",
|
||||
HostType: "LINUX64",
|
||||
MaxSlot: 8,
|
||||
DisptWindow: "always",
|
||||
})
|
||||
|
||||
// Worker push arrives — should merge worker fields, preserve config fields.
|
||||
h.OnHostUpdated(&models.HostInfo{
|
||||
HostName: "node01",
|
||||
HostStatus: 3,
|
||||
CPUFactor: 1.8,
|
||||
NumCPUs: 16,
|
||||
MaxMem: 65536,
|
||||
NumRun: 2,
|
||||
})
|
||||
|
||||
got, ok := store.Hosts.Get("node01")
|
||||
if !ok {
|
||||
t.Fatal("host node01 should exist after merge")
|
||||
}
|
||||
// Worker-sourced fields from the push.
|
||||
if got.HostStatus != 3 {
|
||||
t.Errorf("HostStatus: got %d, want 3", got.HostStatus)
|
||||
}
|
||||
if got.CPUFactor != 1.8 {
|
||||
t.Errorf("CPUFactor: got %f, want 1.8", got.CPUFactor)
|
||||
}
|
||||
if got.NumCPUs != 16 {
|
||||
t.Errorf("NumCPUs: got %d, want 16", got.NumCPUs)
|
||||
}
|
||||
if got.MaxMem != 65536 {
|
||||
t.Errorf("MaxMem: got %d, want 65536", got.MaxMem)
|
||||
}
|
||||
if got.NumRun != 2 {
|
||||
t.Errorf("NumRun: got %d, want 2", got.NumRun)
|
||||
}
|
||||
// Config-sourced fields preserved.
|
||||
if got.HostModel != "x86_64" {
|
||||
t.Errorf("HostModel: got %q, want %q (should be preserved)", got.HostModel, "x86_64")
|
||||
}
|
||||
if got.HostType != "LINUX64" {
|
||||
t.Errorf("HostType: got %q, want %q (should be preserved)", got.HostType, "LINUX64")
|
||||
}
|
||||
if got.MaxSlot != 8 {
|
||||
t.Errorf("MaxSlot: got %d, want 8 (should be preserved)", got.MaxSlot)
|
||||
}
|
||||
if got.DisptWindow != "always" {
|
||||
t.Errorf("DisptWindow: got %q, want %q (should be preserved)", got.DisptWindow, "always")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnHostUpdated_NewHost(t *testing.T) {
|
||||
resetStores()
|
||||
h := DefaultHandler{}
|
||||
|
||||
// No prior entry in store — first push is from worker.
|
||||
h.OnHostUpdated(&models.HostInfo{
|
||||
HostName: "node05",
|
||||
HostStatus: 1,
|
||||
CPUFactor: 3.0,
|
||||
})
|
||||
|
||||
got, ok := store.Hosts.Get("node05")
|
||||
if !ok {
|
||||
t.Fatal("host node05 should exist after first push")
|
||||
}
|
||||
if got.HostStatus != 1 {
|
||||
t.Errorf("HostStatus: got %d, want 1", got.HostStatus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatchConfigPush_CallsOnHostConfigUpdatedForEachHost(t *testing.T) {
|
||||
resetStores()
|
||||
h := &DefaultHandler{}
|
||||
|
||||
hosts := []models.HostInfo{
|
||||
{HostName: "node01", HostModel: "x86_64", HostType: "LINUX64", MaxSlot: 4},
|
||||
{HostName: "node02", HostModel: "arm64", HostType: "LINUX64", MaxSlot: 8},
|
||||
}
|
||||
|
||||
dispatchConfigPush(h, nil, nil, hosts, nil)
|
||||
|
||||
got1, ok1 := store.Hosts.Get("node01")
|
||||
if !ok1 {
|
||||
t.Fatal("node01 should be in store after dispatchConfigPush")
|
||||
}
|
||||
if got1.HostModel != "x86_64" {
|
||||
t.Errorf("node01 HostModel: got %q, want x86_64", got1.HostModel)
|
||||
}
|
||||
if got1.MaxSlot != 4 {
|
||||
t.Errorf("node01 MaxSlot: got %d, want 4", got1.MaxSlot)
|
||||
}
|
||||
|
||||
got2, ok2 := store.Hosts.Get("node02")
|
||||
if !ok2 {
|
||||
t.Fatal("node02 should be in store after dispatchConfigPush")
|
||||
}
|
||||
if got2.HostModel != "arm64" {
|
||||
t.Errorf("node02 HostModel: got %q, want arm64", got2.HostModel)
|
||||
}
|
||||
if got2.MaxSlot != 8 {
|
||||
t.Errorf("node02 MaxSlot: got %d, want 8", got2.MaxSlot)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatchConfigPush_CallsOnClusterUpdated(t *testing.T) {
|
||||
resetStores()
|
||||
h := &DefaultHandler{}
|
||||
|
||||
cluster := &models.ClusterInfo{
|
||||
ClusterName: "mycluster",
|
||||
HeartbeatInterval: 30,
|
||||
Vcpus: 64,
|
||||
InactiveHeartbeatCount: 5,
|
||||
}
|
||||
|
||||
dispatchConfigPush(h, nil, nil, nil, cluster)
|
||||
|
||||
got, ok := store.Clusters.Get("mycluster")
|
||||
if !ok {
|
||||
t.Fatal("mycluster should be in store after dispatchConfigPush")
|
||||
}
|
||||
if got.HeartbeatInterval != 30 {
|
||||
t.Errorf("HeartbeatInterval: got %d, want 30", got.HeartbeatInterval)
|
||||
}
|
||||
if got.Vcpus != 64 {
|
||||
t.Errorf("Vcpus: got %d, want 64", got.Vcpus)
|
||||
}
|
||||
if got.InactiveHeartbeatCount != 5 {
|
||||
t.Errorf("InactiveHeartbeatCount: got %d, want 5", got.InactiveHeartbeatCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatchConfigPush_NilClusterIsSkipped(t *testing.T) {
|
||||
resetStores()
|
||||
h := &DefaultHandler{}
|
||||
|
||||
dispatchConfigPush(h, nil, nil, nil, nil)
|
||||
|
||||
if n := store.Clusters.Count(); n != 0 {
|
||||
t.Errorf("Clusters store should be empty when cluster is nil, got %d entries", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatchConfigPush_EmptySlicesNoOp(t *testing.T) {
|
||||
resetStores()
|
||||
h := &DefaultHandler{}
|
||||
|
||||
dispatchConfigPush(h,
|
||||
[]models.QueueInfo{},
|
||||
[]models.ResourceDefinitionInfo{},
|
||||
[]models.HostInfo{},
|
||||
nil,
|
||||
)
|
||||
|
||||
if n := store.Hosts.Count(); n != 0 {
|
||||
t.Errorf("Hosts store should be empty, got %d entries", n)
|
||||
}
|
||||
if n := store.Queues.Count(); n != 0 {
|
||||
t.Errorf("Queues store should be empty, got %d entries", n)
|
||||
}
|
||||
if n := store.Clusters.Count(); n != 0 {
|
||||
t.Errorf("Clusters store should be empty, got %d entries", n)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue