myxce/src/frontend/apiserver/backend/convert_pure.go
Rongsong Shen bc3103a6d5 feat: P1-1 handle resource value push via OPCode_notify
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
2026-04-17 13:54:03 +00:00

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,
}
}