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:
Rongsong Shen 2026-04-16 11:25:03 +00:00
parent 00c4472d6c
commit 893812e7aa
8 changed files with 850 additions and 11 deletions

View file

@ -107,7 +107,7 @@ func cResourceToModel(res *C.xce_resource_t) *models.ResourceDefinitionInfo {
// cClusterConfQueues extracts queue models from a cluster_config_t.
func cClusterConfQueues(conf *C.cluster_config_t) []models.QueueInfo {
n := int(conf.nqueues)
if n == 0 {
if n == 0 || conf.queues == nil {
return nil
}
qps := unsafe.Slice(conf.queues, n)
@ -123,7 +123,7 @@ func cClusterConfQueues(conf *C.cluster_config_t) []models.QueueInfo {
// cClusterConfResources extracts resource definition models from a cluster_config_t.
func cClusterConfResources(conf *C.cluster_config_t) []models.ResourceDefinitionInfo {
n := int(conf.nres)
if n == 0 {
if n == 0 || conf.resources == nil {
return nil
}
rps := unsafe.Slice(conf.resources, n)
@ -136,6 +136,48 @@ func cClusterConfResources(conf *C.cluster_config_t) []models.ResourceDefinition
return out
}
// cHostConfigToModel extracts C xce_hostconfig_t fields and maps them to HostInfo.
func cHostConfigToModel(hc *C.xce_hostconfig_t) *models.HostInfo {
return rawHostConfigToModel(rawHostConfigData{
name: goString(hc.name),
hostmodel: goString(hc.hostmodel),
hosttype: goString(hc.hosttype),
mxj: int(hc.mxj),
})
}
// cClusterConfHostConfigs extracts host config models from a cluster_config_t.
func cClusterConfHostConfigs(conf *C.cluster_config_t) []models.HostInfo {
n := int(conf.nhconfigs)
if n == 0 || conf.hconfigs == nil {
return nil
}
hps := unsafe.Slice(conf.hconfigs, n)
out := make([]models.HostInfo, 0, n)
for _, hp := range hps {
if hp != nil {
out = append(out, *cHostConfigToModel(hp))
}
}
return out
}
// cClusterConfToClusterInfo extracts C cluster_config_t cluster-level fields and maps them to ClusterInfo.
// Returns nil when clustername is empty so dispatchConfigPush skips the OnClusterUpdated call.
// C struct field names are snake_case (clustername, heartbeat_interval, etc.) — not Cap'n Proto names.
func cClusterConfToClusterInfo(conf *C.cluster_config_t) *models.ClusterInfo {
name := goString(conf.clustername)
if name == "" {
return nil
}
return rawClusterToModel(rawClusterData{
name: name,
heartbeatInterval: int(conf.heartbeat_interval),
vcpus: int(conf.vcpus),
inactiveHeartbeatCount: int(conf.inactive_heartbeat_count),
})
}
// modelJobToSubmit converts a Go JobInfo to C xce_job_submit_t heap structs.
// The caller must free all returned C memory by calling freeJobSubmit.
func modelJobToSubmit(job *models.JobInfo) (*C.xce_job_submit_t, *C.job_submit_data_t, *C.job_startinfo_t) {