fix job array display issue

This commit is contained in:
Rongsong Shen 2026-04-14 18:23:49 +08:00
parent d9b5c361c2
commit 960ef62342
5 changed files with 290 additions and 15 deletions

View file

@ -250,7 +250,8 @@ func SubmitJob(job *models.JobInfo, arraySize int) error {
}
if outJobID != 0 {
job.Unique_JobID = int64(outJobID)
job.JobID = int64(outJobID)
job.JobID = int64(ExtractArrayBaseID(uint64(outJobID)))
job.ArrayIdx = ExtractArrayIdx(uint64(outJobID))
}
return nil
}

View file

@ -5,6 +5,76 @@ import (
"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
@ -77,9 +147,11 @@ type rawResourceData struct {
// 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: r.jobID,
JobID: int64(ExtractArrayBaseID(raw)),
ArrayIdx: ExtractArrayIdx(raw),
JobStatus: r.state,
RunTime: r.runTime,
}

View file

@ -197,3 +197,193 @@ func TestRawResourceToModel_BasicFields(t *testing.T) {
t.Errorf("Interval: got %d, want 60", info.Interval)
}
}
// makeJobID mirrors the C MAKE_JOBID macro in src/include/utils.h.
// Used to construct known 64-bit job IDs for testing.
func makeJobID(cluster, apiserver, timestamp, tag, sequence uint64) uint64 {
return (cluster << clusterIDShift) |
(apiserver << apiserverIDShift) |
(timestamp << timestampShift) |
(tag << tagShift) |
sequence
}
func TestExtractBits(t *testing.T) {
// 0b1111 shifted left by 4 = 0b11110000 = 240
id := uint64(0xF0)
if got := extractBits(id, 4, 4); got != 0xF {
t.Errorf("extractBits(0xF0, 4, 4): got %d, want 15", got)
}
}
func TestExtractIDTag_NonArray(t *testing.T) {
id := makeJobID(1, 0, 1700000000, 0, 5)
if tag := extractIDTag(id); tag != 0x00 {
t.Errorf("tag for non-array job: got %d, want 0", tag)
}
}
func TestExtractIDTag_Array(t *testing.T) {
id := makeJobID(1, 0, 1700000000, tagJobArray, 5)
if tag := extractIDTag(id); tag != tagJobArray {
t.Errorf("tag for array job: got %d, want %d", tag, tagJobArray)
}
}
func TestExtractArrayBaseID_NonArray(t *testing.T) {
// Non-array job: base ID should equal the original ID
id := makeJobID(1, 0, 1700000000, 0, 5)
base := ExtractArrayBaseID(id)
if base != id {
t.Errorf("non-array base ID should equal original: got %d, want %d", base, id)
}
idx := ExtractArrayIdx(id)
if idx != 0 {
t.Errorf("non-array index should be 0: got %d", idx)
}
}
func TestExtractArrayBaseID_ArrayChild(t *testing.T) {
// Array job: cluster=1, apiserver=0, timestamp=1700000000
// The sequence for array jobs has the "job number" in high bits.
// sequence = jobNumber << sequenceHighShift + arrayIdx
// e.g. jobNumber=2, arrayIdx=3 → sequence = 2<<15 + 3 = 65539
jobNumber := uint64(2)
arrayIdx := uint64(3)
sequence := jobNumber<<sequenceHighShift + arrayIdx
id := makeJobID(1, 0, 1700000000, tagJobArray, sequence)
base := ExtractArrayBaseID(id)
// Base ID should have tag=0, same cluster/apiserver/timestamp, and arrayJob in sequence
expectedBase := makeJobID(1, 0, 1700000000, 0, jobNumber<<sequenceHighShift)
if base != expectedBase {
t.Errorf("array base ID mismatch: got %d, want %d", base, expectedBase)
}
idx := ExtractArrayIdx(id)
if idx != int(arrayIdx) {
t.Errorf("array index: got %d, want %d", idx, arrayIdx)
}
}
func TestExtractArrayIdx_MaxIndex(t *testing.T) {
// Maximum array index = 32767 (2^15 - 1)
maxIdx := uint64((1 << nArrayIdxBits) - 1)
sequence := uint64(1)<<sequenceHighShift + maxIdx
id := makeJobID(1, 0, 1700000000, tagJobArray, sequence)
idx := ExtractArrayIdx(id)
if idx != int(maxIdx) {
t.Errorf("max array index: got %d, want %d", idx, maxIdx)
}
}
func TestExtractArrayBaseID_RoundTrip(t *testing.T) {
// ExtractArrayBaseID and ExtractArrayIdx together should preserve enough
// information to reconstruct the original ID.
// Reconstruction requires re-encoding with tag=TAG_JOB_ARRAY.
jobNumber := uint64(7)
arrayIdx := uint64(42)
sequence := jobNumber<<sequenceHighShift + arrayIdx
id := makeJobID(1, 0, 1700000000, tagJobArray, sequence)
base := ExtractArrayBaseID(id)
idx := ExtractArrayIdx(id)
// Reconstruct: base ID sequence + array index, with array tag set
reconstructedSeq := (extractBits(base, sequenceHighShift, nSequenceBits-nArrayIdxBits) << sequenceHighShift) + uint64(idx)
reconstructed := makeJobID(
extractBits(base, clusterIDShift, nClusterIDBits),
extractBits(base, apiserverIDShift, nApiserverIDBits),
extractBits(base, timestampShift, nTimestampBits),
tagJobArray,
reconstructedSeq,
)
if reconstructed != id {
t.Errorf("round-trip failed: reconstructed %d, want %d", reconstructed, id)
}
}
func TestRawJobToModel_NonArrayJob(t *testing.T) {
// Non-array job: JobID == Unique_JobID, ArrayIdx == 0
id := makeJobID(1, 0, 1700000000, 0, 5)
r := rawJobData{jobID: int64(id), state: 2}
info := rawJobToModel(r)
if info.Unique_JobID != int64(id) {
t.Errorf("Unique_JobID: got %d, want %d", info.Unique_JobID, id)
}
if info.JobID != int64(id) {
t.Errorf("JobID for non-array should equal Unique_JobID: got %d, want %d", info.JobID, id)
}
if info.ArrayIdx != 0 {
t.Errorf("ArrayIdx for non-array should be 0: got %d", info.ArrayIdx)
}
}
func TestRawJobToModel_ArrayChild(t *testing.T) {
// Array child: JobID == baseID, ArrayIdx == arrayIdx
jobNumber := uint64(2)
arrayIdx := uint64(5)
sequence := jobNumber<<sequenceHighShift + arrayIdx
id := makeJobID(1, 0, 1700000000, tagJobArray, sequence)
baseID := makeJobID(1, 0, 1700000000, 0, jobNumber<<sequenceHighShift)
r := rawJobData{jobID: int64(id), state: 2}
info := rawJobToModel(r)
if info.Unique_JobID != int64(id) {
t.Errorf("Unique_JobID: got %d, want %d", info.Unique_JobID, id)
}
if info.JobID != int64(baseID) {
t.Errorf("JobID for array child should be base ID: got %d, want %d", info.JobID, baseID)
}
if info.ArrayIdx != int(arrayIdx) {
t.Errorf("ArrayIdx for array child: got %d, want %d", info.ArrayIdx, arrayIdx)
}
}
func TestRawJobToModel_ArrayChildrenShareBaseID(t *testing.T) {
// All array children should share the same base JobID
jobNumber := uint64(3)
baseID := makeJobID(1, 0, 1700000000, 0, jobNumber<<sequenceHighShift)
for idx := 0; idx < 5; idx++ {
sequence := jobNumber<<sequenceHighShift + uint64(idx)
id := makeJobID(1, 0, 1700000000, tagJobArray, sequence)
r := rawJobData{jobID: int64(id)}
info := rawJobToModel(r)
if info.JobID != int64(baseID) {
t.Errorf("array child %d: JobID %d should equal base ID %d", idx, info.JobID, baseID)
}
if info.ArrayIdx != idx {
t.Errorf("array child %d: ArrayIdx got %d, want %d", idx, info.ArrayIdx, idx)
}
if info.Unique_JobID != int64(id) {
t.Errorf("array child %d: Unique_JobID should be unique per child", idx)
}
}
}
func TestIsArrayJob(t *testing.T) {
// Non-array job (tag=0x00): IsArrayJob returns false
nonArrayID := makeJobID(1, 0, 1700000000, 0, 5)
if IsArrayJob(nonArrayID) {
t.Errorf("non-array job should not be an array job")
}
// Array child (tag=0x01): IsArrayJob returns true
arrayID := makeJobID(1, 0, 1700000000, tagJobArray, 5)
if !IsArrayJob(arrayID) {
t.Errorf("array job child should be an array job")
}
// Array child with idx=0: IsArrayJob returns true
jobNumber := uint64(2)
sequence := jobNumber<<sequenceHighShift + 0 // arrayIdx=0
arrayIdx0ID := makeJobID(1, 0, 1700000000, tagJobArray, sequence)
if !IsArrayJob(arrayIdx0ID) {
t.Errorf("array child with idx=0 should still be an array job")
}
}

View file

@ -1192,8 +1192,8 @@ func (j JobController) RestartJobByID(ctx *gin.Context) {
// Use a concrete type for job
newJob := models.JobInfo{
Unique_JobID: int64(jobID),
JobID: int64(jobID),
ArrayIdx: 0,
JobID: int64(backend.ExtractArrayBaseID(jobID)),
ArrayIdx: backend.ExtractArrayIdx(jobID),
SubmitUser: restarter,
JobStatus: models.JOBSTAT["PEND"],
}
@ -1453,7 +1453,8 @@ func (j JobController) CreateJob(ctx *gin.Context) {
// Build the single JobInfo to submit
job := models.JobInfo{
Unique_JobID: primaryJobID,
JobID: primaryJobID,
JobID: int64(backend.ExtractArrayBaseID(uint64(primaryJobID))),
ArrayIdx: backend.ExtractArrayIdx(uint64(primaryJobID)),
//SubmitTime: time.Now().Unix(), // backend will fill the submit time;
SubmitUser: submitUser,
JobStatus: models.JOBSTAT["PEND"], // new job is pend.
@ -3097,12 +3098,20 @@ func processJobData(rows []utils.OrderMap, pagetoken string) utils.QueryResult {
}
}
// Update job_name if array_idx is not 0
// Update job_name if this is an array job child (including idx=0)
if jobName, exist := row.Data["job_name"]; exist {
if arrayIdx, ok := row.Data["array_idx"].(int); ok && arrayIdx > 0 {
row.Data["job_name"] = fmt.Sprintf("%s[%d]", jobName, arrayIdx)
} else if arrayIdx64, ok := row.Data["array_idx"].(int64); ok && arrayIdx64 > 0 {
row.Data["job_name"] = fmt.Sprintf("%s[%d]", jobName, arrayIdx64)
// Use IsArrayJob to detect array children, which covers
// the first child (idx=0) that the old array_idx > 0 check missed.
var isArray bool
if uid, ok := row.Data["unique_job_id"].(int64); ok {
isArray = backend.IsArrayJob(uint64(uid))
}
if isArray {
if arrayIdx, ok := row.Data["array_idx"].(int); ok {
row.Data["job_name"] = fmt.Sprintf("%s[%d]", jobName, arrayIdx)
} else if arrayIdx64, ok := row.Data["array_idx"].(int64); ok {
row.Data["job_name"] = fmt.Sprintf("%s[%d]", jobName, arrayIdx64)
}
} else {
row.Data["job_name"] = jobName
}

View file

@ -83,7 +83,11 @@ func (p *JobsPagePrinter) initColumnWidths(jobs *models.JobsResponse) {
width := 0
switch format.name {
case models.HEADER_JOB_JOBID:
width = len(fmt.Sprintf("%d", job.JobID))
if job.UniqueJobID != job.JobID {
width = len(fmt.Sprintf("%d[%d]", job.JobID, job.ArrayIdx))
} else {
width = len(fmt.Sprintf("%d", job.JobID))
}
case models.HEADER_JOB_USER:
width = len(job.SubmitUser)
case models.HEADER_JOB_STATUS:
@ -174,7 +178,7 @@ func (p *JobsPagePrinter) formatRows(jobs *models.JobsResponse, truncatFlag bool
switch format.name {
case models.HEADER_JOB_JOBID:
var arrayJobIDStr string
if job.ArrayIdx != 0 {
if job.UniqueJobID != job.JobID {
arrayJobIDStr = fmt.Sprintf("%d[%d]", job.JobID, job.ArrayIdx)
} else {
arrayJobIDStr = fmt.Sprintf("%d", job.JobID)
@ -274,9 +278,8 @@ func printJobsVerboseOutput(jobs *models.JobsResponse, filter []string) {
}
// generate jobId for two format: 111 or 111[10]
jobId := fmt.Sprintf("%d", job.JobID)
index := strings.Index(job.JobName, "[")
if index != -1 {
jobId = fmt.Sprintf("%s%s", jobId, job.JobName[index:])
if job.UniqueJobID != job.JobID {
jobId = fmt.Sprintf("%d[%d]", job.JobID, job.ArrayIdx)
}
// (1) print the job baisc info when sending "jobs list -l" command