package backend import ( "apiserver/models" "encoding/json" "time" "fmt" ) // 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 } // FormatJobID formats a packed 64-bit job ID as a human-readable string, // matching the C fmt_jobid() output in src/lib/utils.c. // - Regular job: "{cluster}-{apiserver}-{timestamp}-{sequence}" // - Array child: "{cluster}-{apiserver}-{timestamp}-{base}[{idx}]" func FormatJobID(uniqueJobID int64) string { id := uint64(uniqueJobID) clusterID := extractBits(id, clusterIDShift, nClusterIDBits) apiserverID := extractBits(id, apiserverIDShift, nApiserverIDBits) timestamp := extractBits(id, timestampShift, nTimestampBits) if extractIDTag(id) == tagJobArray { arrayBase := extractBits(id, sequenceHighShift, nSequenceBits-nArrayIdxBits) << uint(sequenceHighShift) arrayIdx := extractBits(id, 0, nArrayIdxBits) return fmt.Sprintf("%d-%d-%d-%d[%d]", clusterID, apiserverID, timestamp, arrayBase, arrayIdx) } sequence := extractBits(id, 0, nSequenceBits) return fmt.Sprintf("%d-%d-%d-%d", clusterID, apiserverID, timestamp, sequence) } // WorkerDefaultOutputPath returns the output file path the worker uses by default // for the given job ID and task index, matching uv_proc.c make_job_output_path(). func WorkerDefaultOutputPath(uniqueJobID int64, taskIdx int) string { return fmt.Sprintf("/tmp/job_%s_task%d.out", FormatJobID(uniqueJobID), taskIdx) } // 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 resvals []models.ResourceDistributionInfo } // 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 } // rawJobArrayToModels expands one compact (base jobid, array_size, one // shared spec) array descriptor into N models.JobInfo entries locally, one // per index in [0, array_size), using base_jobid + i arithmetic — the same // arithmetic the controller's restore_job_array() (job.c) and the // scheduler's sched_on_job_array() (handler.c) use to derive each // element's identity (job-array-single-wal-record task 5.4, Req 6.2, 6.5). // A freshly-received array element mirrors sched_on_job_array's // make_array_element: JOB_STAT_PEND, receive-time sub_time — the compact // wire descriptor carries no per-element timestamps to recover. // Returns nil for a zero array_size or a nil submit (nothing to expand). // Pure Go (no CGO) — cJobArrayToModels (convert.go) is the CGO adapter // that decodes a *C.xce_job_submit_t into the rawSubmitData this consumes. func rawJobArrayToModels(baseJobID uint64, arraySize uint64, submit *rawSubmitData) []*models.JobInfo { if arraySize == 0 || submit == nil { return nil } now := time.Now().Unix() out := make([]*models.JobInfo, 0, arraySize) for i := uint64(0); i < arraySize; i++ { r := rawJobData{ jobID: int64(baseJobID + i), state: models.JOBSTAT["PEND"], subTime: now, submit: submit, } out = append(out, rawJobToModel(r)) } return out } // rawWorkerToModel maps a rawWorkerData to a models.HostInfo. func rawWorkerToModel(r rawWorkerData) *models.HostInfo { info := &models.HostInfo{ HostName: r.name, HostStatus: r.status, ResourceValues: r.resvals, } 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, } }