myxce/src/frontend/apiserver/backend/convert_pure.go
Rongsong Shen ce2edf0dac refactor: represent time fields as int64 Unix seconds throughout the stack
Collapses a four-step conversion chain (time_t → int64 → time.Time →
RFC3339 string → string → time.Time) to a direct int64 pass-through:

  C time_t → rawJobData int64 → JobInfo int64 → JSON integer → QueryJob int64 → time.Unix() for display

Changes:
- apiserver/models/jobs.go: SubmitTime, QueueTime, StartTime, EndTime,
  ReserveTime in JobInfo and JobAdditionalFields: time.Time → int64
- apiserver/models/common.go: ActionControl.SubmitTime: time.Time → int64
- backend/convert_pure.go: remove time.Unix() calls; assign int64 directly
- backend/convert_test.go: update time assertions to compare int64 values
- xcecli/models/jobs.go: SubmitTime, StartTime, EndTime, ReserveTime in
  QueryJob: string/time.Time → int64; remove time import
- xcecli/common/output/time.go: replace parseJobTime(string) with
  unixToTime(int64) and fmtUnixTime(int64) helpers
- xcecli/common/output/page_print_jobs.go: replace parseJobTime() calls
  with unixToTime()/fmtUnixTime(); remove !startTime.IsZero() guard
  (unixToTime returns false for ts<=0, making it redundant)
- xcecli/common/output/page_print_array_elements_jobs.go: replace
  string-split+time.Parse submit-time block with fmtUnixTime(); remove
  unused time import

Side effect: eliminates the "0001-01-01T00:00:00Z" zero-time
serialization issue at the source — zero timestamps are now 0 (integer)
and unixToTime/fmtUnixTime return false/"-" for ts<=0 without special-
casing needed in callers.
2026-05-09 07:00:49 +00:00

348 lines
10 KiB
Go

package backend
import (
"apiserver/models"
"encoding/json"
)
// 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 | 5 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 (== N_ARRAY_IDX_BITS)
nSequenceBits = 20 // N_JOBID_BITS(64) - N_CLUSTER_ID_BITS(6) - N_APISERVER_ID_BITS(4) - N_TIMESTAMP_BITS(32) - N_TAG_BITS(2) = 20
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
execHosts []string
}
// 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
}
}
info.SubmitTime = r.subTime
info.QueueTime = r.queueTime
info.StartTime = r.startTime
info.EndTime = r.finishTime
if len(r.execHosts) > 0 {
type hostSlot struct {
HostName string `json:"host_name"`
NumSlots int `json:"num_slots"`
}
type execHostsJSON struct {
HostSlots []hostSlot `json:"host_slots"`
NumHosts int `json:"num_hosts"`
}
slots := make([]hostSlot, len(r.execHosts))
for i, h := range r.execHosts {
slots[i] = hostSlot{HostName: h, NumSlots: 1}
}
data, _ := json.Marshal(execHostsJSON{HostSlots: slots, NumHosts: len(slots)})
info.ExecHosts = models.Jsonb{String: string(data)}
}
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,
}
}