diff --git a/src/frontend/apiserver/backend/clib.go b/src/frontend/apiserver/backend/clib.go index 51a848d..bc5d599 100644 --- a/src/frontend/apiserver/backend/clib.go +++ b/src/frontend/apiserver/backend/clib.go @@ -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 } diff --git a/src/frontend/apiserver/backend/convert_pure.go b/src/frontend/apiserver/backend/convert_pure.go index 479561a..e228a85 100644 --- a/src/frontend/apiserver/backend/convert_pure.go +++ b/src/frontend/apiserver/backend/convert_pure.go @@ -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, } diff --git a/src/frontend/apiserver/backend/convert_test.go b/src/frontend/apiserver/backend/convert_test.go index 10f51c1..9cb0088 100644 --- a/src/frontend/apiserver/backend/convert_test.go +++ b/src/frontend/apiserver/backend/convert_test.go @@ -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< 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 } diff --git a/src/frontend/xcecli/common/output/page_print_jobs.go b/src/frontend/xcecli/common/output/page_print_jobs.go index b58c7a1..e64b647 100644 --- a/src/frontend/xcecli/common/output/page_print_jobs.go +++ b/src/frontend/xcecli/common/output/page_print_jobs.go @@ -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