Add OPCode_notify handler in go_push_handler top-level switch (not inside handlePush) to receive resource value updates from the controller via xce_notify_data_t/schedUpdateResvals. - Add 8 CGO union accessors for xce_val_t (which, i32, i64, u32, u64, fval, str, bool) since CGO cannot access union fields - Add handleNotifyData() dispatching NotifyData_data_schedUpdateResvals events, iterating xce_resval_list_t and calling cResvalToDistribution - Add cResvalToDistribution in convert.go with 7 separate switch cases for Value_u_* union discriminants (no combining i32/i64) - Add rawResvalData + rawResvalToDistribution pure-Go version in convert_pure.go for testability without CGO - Add 10 TDD tests covering all 7 value types, nil val guard, unknown discriminant, and boolean false - All 42 backend tests passing
339 lines
10 KiB
Go
339 lines
10 KiB
Go
package backend
|
|
|
|
import (
|
|
"apiserver/models"
|
|
"time"
|
|
)
|
|
|
|
// JobID bit layout constants — mirrors src/include/config.h.
|
|
//
|
|
// |63 62|61 56|55 52|51 20|19 15|14 0|
|
|
// | TAG | CLUSTER_ID | APISERVER_ID| TIMESTAMP | SEQ_HIGH | ARRAY_IDX |
|
|
// | 2 bits | 6 bits | 4 bits | 32 bits | 15 bits | 15 bits |
|
|
const (
|
|
nTagBits = 2
|
|
tagShift = 62
|
|
nClusterIDBits = 6
|
|
clusterIDShift = 56
|
|
nApiserverIDBits = 4
|
|
apiserverIDShift = 52
|
|
nTimestampBits = 32
|
|
timestampShift = 20
|
|
nArrayIdxBits = 15
|
|
sequenceHighShift = 15 // mirrors SEQUENCE_HIGH_SHIFT in config.h
|
|
nSequenceBits = 30 // mirrors N_SEQUENCE_BITS in config.h
|
|
tagJobArray = 0x01 // mirrors TAG_JOB_ARRAY in config.h
|
|
)
|
|
|
|
// extractBits extracts a bit field from id at the given shift position.
|
|
// Mirrors EXTRACT_BITS(id, shift, bits) in src/include/utils.h.
|
|
func extractBits(id uint64, shift, bits int) uint64 {
|
|
mask := uint64((1 << bits) - 1)
|
|
return (id >> shift) & mask
|
|
}
|
|
|
|
// extractIDTag extracts the 2-bit tag field (bits 62-63).
|
|
// Mirrors EXTRACT_ID_TAG in src/include/utils.h.
|
|
func extractIDTag(id uint64) uint64 {
|
|
return extractBits(id, tagShift, nTagBits)
|
|
}
|
|
|
|
// ExtractArrayBaseID extracts the base (family) job ID from a 64-bit encoded
|
|
// jobid. Mirrors C's array_jobid() in src/lib/utils.c and EXTRACT_ARRAY_BASEID
|
|
// in src/include/utils.h.
|
|
// - For non-array jobs (tag=0x00): returns id unchanged.
|
|
// - For array children (tag=0x01): returns the shared base ID (tag=0, idx=0).
|
|
func ExtractArrayBaseID(id uint64) uint64 {
|
|
if extractIDTag(id) == 0x00 {
|
|
return id
|
|
}
|
|
clusterID := extractBits(id, clusterIDShift, nClusterIDBits)
|
|
apiserverID := extractBits(id, apiserverIDShift, nApiserverIDBits)
|
|
timestamp := extractBits(id, timestampShift, nTimestampBits)
|
|
// EXTRACT_ARRAY_JOB: high bits of sequence (mirrors config.h SEQUENCE_HIGH_SHIFT)
|
|
arrayJob := extractBits(id, sequenceHighShift, nSequenceBits-nArrayIdxBits) << sequenceHighShift
|
|
return (clusterID << clusterIDShift) |
|
|
(apiserverID << apiserverIDShift) |
|
|
(timestamp << timestampShift) |
|
|
arrayJob // tag=0, idx=0
|
|
}
|
|
|
|
// ExtractArrayIdx extracts the array index from a 64-bit encoded jobid.
|
|
// Mirrors C's array_idx() in src/lib/utils.c and EXTRACT_ARRAY_IDX in
|
|
// src/include/utils.h.
|
|
// Returns 0 for non-array jobs (tag=0x00).
|
|
func ExtractArrayIdx(id uint64) int {
|
|
if extractIDTag(id) == 0x00 {
|
|
return 0
|
|
}
|
|
return int(extractBits(id, 0, nArrayIdxBits))
|
|
}
|
|
|
|
// IsArrayJob reports whether the 64-bit encoded jobid belongs to an array job
|
|
// (tag == TAG_JOB_ARRAY, 0x01). Returns false for non-array jobs (tag=0x00).
|
|
func IsArrayJob(id uint64) bool {
|
|
return extractIDTag(id) == tagJobArray
|
|
}
|
|
|
|
// rawStartInfo mirrors the C job_startinfo_t fields used by the Go model.
|
|
type rawStartInfo struct {
|
|
cmdline string
|
|
launchType string
|
|
preExec string
|
|
postExec string
|
|
execHome string
|
|
execCWD string
|
|
execUser string
|
|
}
|
|
|
|
// rawSubmitData mirrors the C job_submit_data_t fields used by the Go model.
|
|
type rawSubmitData struct {
|
|
queue string
|
|
user string
|
|
jobName string
|
|
projectName string
|
|
userGroup string
|
|
jobDesc string
|
|
resReq string
|
|
minTasks int
|
|
maxTasks int
|
|
startInfo *rawStartInfo
|
|
}
|
|
|
|
// rawJobData mirrors the C xce_job_t fields used by the Go model.
|
|
type rawJobData struct {
|
|
jobID int64
|
|
state int
|
|
runTime int
|
|
subTime int64
|
|
queueTime int64
|
|
startTime int64
|
|
finishTime int64
|
|
submit *rawSubmitData
|
|
}
|
|
|
|
// rawWorkerData mirrors the C xce_worker_t fields used by the Go model.
|
|
type rawWorkerData struct {
|
|
name string
|
|
status int
|
|
hasAttrs bool
|
|
hostType string
|
|
hostModel string
|
|
cpuFactor float64
|
|
maxCPUs int
|
|
maxMem int
|
|
maxSwap int
|
|
maxTmp int
|
|
nDisks int
|
|
}
|
|
|
|
// rawQueueData mirrors the C xce_queue_t fields used by the Go model.
|
|
type rawQueueData struct {
|
|
name string
|
|
status int
|
|
priority int
|
|
}
|
|
|
|
// rawResourceData mirrors the C xce_resource_t fields used by the Go model.
|
|
type rawResourceData struct {
|
|
name string
|
|
desc string
|
|
valType int
|
|
ordType int
|
|
flags int
|
|
interval int
|
|
}
|
|
|
|
// rawJobToModel maps a rawJobData to a models.JobInfo.
|
|
// This pure-Go function contains the field-mapping logic and is tested without CGO.
|
|
func rawJobToModel(r rawJobData) *models.JobInfo {
|
|
raw := uint64(r.jobID)
|
|
info := &models.JobInfo{
|
|
Unique_JobID: r.jobID,
|
|
JobID: int64(ExtractArrayBaseID(raw)),
|
|
ArrayIdx: ExtractArrayIdx(raw),
|
|
JobStatus: r.state,
|
|
RunTime: r.runTime,
|
|
}
|
|
if r.submit != nil {
|
|
s := r.submit
|
|
info.QueueName = s.queue
|
|
info.SubmitUser = s.user
|
|
info.JobName = s.jobName
|
|
info.ProjectName = s.projectName
|
|
info.UserGroup = s.userGroup
|
|
info.JobDescription = s.jobDesc
|
|
info.ResReq = s.resReq
|
|
info.NumProcessors = s.minTasks
|
|
info.MaxNumProcessors = s.maxTasks
|
|
if s.startInfo != nil {
|
|
si := s.startInfo
|
|
info.Command = si.cmdline
|
|
info.LaunchType = si.launchType
|
|
info.PreExecCmd = si.preExec
|
|
info.PostExecCmd = si.postExec
|
|
info.ExecHome = si.execHome
|
|
info.ExecCWD = si.execCWD
|
|
info.ExecUser = si.execUser
|
|
}
|
|
}
|
|
if r.subTime > 0 {
|
|
info.SubmitTime = time.Unix(r.subTime, 0)
|
|
}
|
|
if r.queueTime > 0 {
|
|
info.QueueTime = time.Unix(r.queueTime, 0)
|
|
}
|
|
if r.startTime > 0 {
|
|
info.StartTime = time.Unix(r.startTime, 0)
|
|
}
|
|
if r.finishTime > 0 {
|
|
info.EndTime = time.Unix(r.finishTime, 0)
|
|
}
|
|
return info
|
|
}
|
|
|
|
// rawWorkerToModel maps a rawWorkerData to a models.HostInfo.
|
|
func rawWorkerToModel(r rawWorkerData) *models.HostInfo {
|
|
info := &models.HostInfo{
|
|
HostName: r.name,
|
|
HostStatus: r.status,
|
|
}
|
|
if r.hasAttrs {
|
|
info.HostType = r.hostType
|
|
info.HostModel = r.hostModel
|
|
info.CPUFactor = r.cpuFactor
|
|
info.NumCPUs = r.maxCPUs
|
|
info.MaxMem = r.maxMem
|
|
info.MaxSwap = r.maxSwap
|
|
info.MaxTmp = r.maxTmp
|
|
info.NumDisk = r.nDisks
|
|
}
|
|
return info
|
|
}
|
|
|
|
// rawQueueToModel maps a rawQueueData to a models.QueueInfo.
|
|
func rawQueueToModel(r rawQueueData) *models.QueueInfo {
|
|
return &models.QueueInfo{
|
|
QueueName: r.name,
|
|
QueueStatus: r.status,
|
|
Priority: r.priority,
|
|
}
|
|
}
|
|
|
|
// rawResourceToModel maps a rawResourceData to a models.ResourceDefinitionInfo.
|
|
func rawResourceToModel(r rawResourceData) *models.ResourceDefinitionInfo {
|
|
return &models.ResourceDefinitionInfo{
|
|
ResourceName: r.name,
|
|
Description: r.desc,
|
|
ValueType: r.valType,
|
|
OrderType: r.ordType,
|
|
Flag: r.flags,
|
|
Interval: r.interval,
|
|
}
|
|
}
|
|
|
|
// rawHostConfigData mirrors the C xce_hostconfig_t fields used by the Go model.
|
|
// Only fields that map to models.HostInfo are included:
|
|
// - name → HostName, hostmodel → HostModel, hosttype → HostType, mxj → MaxSlot
|
|
//
|
|
// resources (xce_config_t**) and thresholds (xce_res_threshold_t**) have no HostInfo field and are excluded.
|
|
// dispatchwindow (xce_timewindow_config_t*) is deferred — DisptWindow is left as empty string.
|
|
type rawHostConfigData struct {
|
|
name string
|
|
hostmodel string
|
|
hosttype string
|
|
mxj int
|
|
}
|
|
|
|
// rawHostConfigToModel maps a rawHostConfigData to a models.HostInfo.
|
|
// Config-sourced fields only; worker-sourced fields remain zero.
|
|
func rawHostConfigToModel(r rawHostConfigData) *models.HostInfo {
|
|
return &models.HostInfo{
|
|
HostName: r.name,
|
|
HostModel: r.hostmodel,
|
|
HostType: r.hosttype,
|
|
MaxSlot: r.mxj,
|
|
// TODO(dispatchwindow): xce_hostconfig_t.dispatchwindow is xce_timewindow_config_t*
|
|
// (nsegs int + segs[] with open/close time fields). Serializing it requires a Go
|
|
// mirror struct and json.Marshal. Deferred: no API endpoint currently consumes
|
|
// DisptWindow. Implement when /xce/v1/hosts returns dispatch window data.
|
|
}
|
|
}
|
|
|
|
// rawResvalData mirrors the C xce_resval_t fields used by the Go model.
|
|
// IsDynamic is always true for push-sourced values (controller sends live measurements).
|
|
type rawResvalData struct {
|
|
name string
|
|
host string
|
|
valWhich int // mirrors C xce_val_t.which (Value_u_* discriminant)
|
|
i32 int32
|
|
i64 int64
|
|
u32 uint32
|
|
u64 uint64
|
|
fval float64
|
|
sval string
|
|
bval bool
|
|
valNil bool // true when the C xce_val_t* was NULL
|
|
}
|
|
|
|
// rawResvalToDistribution maps a rawResvalData to a models.ResourceDistributionInfo.
|
|
// Returns nil when valNil is true (C val pointer was NULL) or when valWhich
|
|
// is an unknown discriminant — callers should skip nil results.
|
|
func rawResvalToDistribution(r rawResvalData) *models.ResourceDistributionInfo {
|
|
rd := &models.ResourceDistributionInfo{
|
|
ResourceName: r.name,
|
|
Location: r.host,
|
|
IsDynamic: true, // resource values pushed from workers are always dynamic
|
|
}
|
|
if r.valNil {
|
|
return nil
|
|
}
|
|
switch r.valWhich {
|
|
case 0: // Value_u_ivalue32
|
|
rd.ValueType = int(models.NUMERIC)
|
|
rd.NumericValue = float64(r.i32)
|
|
case 2: // Value_u_ivalue64
|
|
rd.ValueType = int(models.NUMERIC)
|
|
rd.NumericValue = float64(r.i64)
|
|
case 1: // Value_u_uvalue32
|
|
rd.ValueType = int(models.NUMERIC)
|
|
rd.NumericValue = float64(r.u32)
|
|
case 3: // Value_u_uvalue64
|
|
rd.ValueType = int(models.NUMERIC)
|
|
rd.NumericValue = float64(r.u64)
|
|
case 5: // Value_u_fvalue
|
|
rd.ValueType = int(models.NUMERIC)
|
|
rd.NumericValue = r.fval
|
|
case 4: // Value_u_svalue
|
|
rd.ValueType = int(models.STRING)
|
|
rd.StringValue = r.sval
|
|
case 6: // Value_u_bvalue
|
|
rd.ValueType = int(models.BOOLEAN)
|
|
rd.BooleanValue = r.bval
|
|
default:
|
|
return nil
|
|
}
|
|
return rd
|
|
}
|
|
|
|
// rawClusterData mirrors the C cluster_config_t fields used by the Go model.
|
|
// Only config-sourced fields are included (heartbeat_interval, vcpus, inactive_heartbeat_count).
|
|
// Built-in/sourced fields (Version, Status, ControllerName, etc.) come from LoadBuiltinData
|
|
// and are not part of the config push conversion.
|
|
type rawClusterData struct {
|
|
name string
|
|
heartbeatInterval int
|
|
vcpus int
|
|
inactiveHeartbeatCount int
|
|
}
|
|
|
|
// rawClusterToModel maps a rawClusterData to a models.ClusterInfo.
|
|
// Config-sourced fields only; built-in fields remain zero.
|
|
func rawClusterToModel(r rawClusterData) *models.ClusterInfo {
|
|
return &models.ClusterInfo{
|
|
ClusterName: r.name,
|
|
HeartbeatInterval: r.heartbeatInterval,
|
|
Vcpus: r.vcpus,
|
|
InactiveHeartbeatCount: r.inactiveHeartbeatCount,
|
|
}
|
|
}
|