From d6008ead99e49dfa46a7ade697e4687a4898155c Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Thu, 7 May 2026 02:36:24 +0000 Subject: [PATCH] improve frontend jobid display --- src/frontend/xcecli/cmd/jobs/controller.go | 24 +-- src/frontend/xcecli/common/format/jobid.go | 148 ++++++++++++++++++ .../xcecli/common/format/jobid_test.go | 117 ++++++++++++++ .../output/page_print_array_elements_jobs.go | 9 +- .../xcecli/common/output/page_print_jobs.go | 21 +-- src/frontend/xcecli/filters/job_filter.go | 27 +++- 6 files changed, 305 insertions(+), 41 deletions(-) create mode 100644 src/frontend/xcecli/common/format/jobid.go create mode 100644 src/frontend/xcecli/common/format/jobid_test.go diff --git a/src/frontend/xcecli/cmd/jobs/controller.go b/src/frontend/xcecli/cmd/jobs/controller.go index 71e53bdf..7a7241de 100755 --- a/src/frontend/xcecli/cmd/jobs/controller.go +++ b/src/frontend/xcecli/cmd/jobs/controller.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "strings" + "xcecli/common/format" "xcecli/common/request" "xcecli/config" "xcecli/models" @@ -47,8 +48,15 @@ func (c *JobController) RunJobsControl() error { // control jobs by job IDs func (c *JobController) controlJobsByIDs(apiClient *request.APIClient, token string, certPath string) error { - jobIDsStr := strings.Join(c.jobIDs, ",") - url := fmt.Sprintf("/xce/v1/jobs/%s/control", jobIDsStr) + apiIDs := make([]string, 0, len(c.jobIDs)) + for _, id := range c.jobIDs { + apiID, err := format.ToAPIJobID(strings.TrimSpace(id)) + if err != nil { + return fmt.Errorf("invalid job ID %q: %w", id, err) + } + apiIDs = append(apiIDs, apiID) + } + url := fmt.Sprintf("/xce/v1/jobs/%s/control", strings.Join(apiIDs, ",")) req := models.JobControlJobIDRequest{ Action: c.action, @@ -88,19 +96,11 @@ func (c *JobController) printJobControlResponse(req interface{}, apiClient *requ return err } for _, data := range resp.Data.Accepted { - if data.ArrayIdx != 0 { - fmt.Printf("job <%d[%d]> %s\n", data.JobID, data.ArrayIdx, strings.TrimPrefix(data.Msg, "job ")) - } else { - fmt.Printf("job <%d> %s\n", data.JobID, strings.TrimPrefix(data.Msg, "job ")) - } + fmt.Printf("job <%s> %s\n", format.FormatArrayChildID(data.JobID, data.ArrayIdx), strings.TrimPrefix(data.Msg, "job ")) } for _, data := range resp.Data.Failed { - if data.ArrayIdx != 0 { - fmt.Printf("job <%d[%d]> %s\n", data.JobID, data.ArrayIdx, strings.TrimPrefix(data.Msg, "job ")) - } else { - fmt.Printf("job <%d> %s\n", data.JobID, strings.TrimPrefix(data.Msg, "job ")) - } + fmt.Printf("job <%s> %s\n", format.FormatArrayChildID(data.JobID, data.ArrayIdx), strings.TrimPrefix(data.Msg, "job ")) } return nil } diff --git a/src/frontend/xcecli/common/format/jobid.go b/src/frontend/xcecli/common/format/jobid.go new file mode 100644 index 00000000..a337a32b --- /dev/null +++ b/src/frontend/xcecli/common/format/jobid.go @@ -0,0 +1,148 @@ +package format + +import ( + "fmt" + "regexp" + "strconv" +) + +// 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 +) + +// 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 +} + +// FormatJobID decomposes a 64-bit packed job ID and formats it as a +// human-readable string matching the C fmt_jobid() format 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 (extractBits(id, tagShift, nTagBits) & tagJobArray) != 0 { + // arrayBase mirrors EXTRACT_ARRAY_JOB: the seq_high bits shifted back to their + // original position, matching C's fmt_jobid_ex output exactly. + 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) +} + +// IsArrayJob returns true if the 64-bit packed job ID has the array job tag set. +func IsArrayJob(uniqueJobID int64) bool { + return extractBits(uint64(uniqueJobID), tagShift, nTagBits)&tagJobArray != 0 +} + +// DecomposeJobID splits a 64-bit packed job ID into its component fields. +func DecomposeJobID(uniqueJobID int64) (clusterID, apiserverID, timestamp, sequence uint32, isArray bool, arrayIdx uint32) { + id := uint64(uniqueJobID) + clusterID = uint32(extractBits(id, clusterIDShift, nClusterIDBits)) + apiserverID = uint32(extractBits(id, apiserverIDShift, nApiserverIDBits)) + timestamp = uint32(extractBits(id, timestampShift, nTimestampBits)) + isArray = extractBits(id, tagShift, nTagBits)&tagJobArray != 0 + sequence = uint32(extractBits(id, 0, nSequenceBits)) + arrayIdx = uint32(extractBits(id, 0, nArrayIdxBits)) + return +} + +// MakeJobID packs the components into a 64-bit job ID, mirroring MAKE_JOBID in src/include/utils.h. +func MakeJobID(clusterID, apiserverID, timestamp uint64, tag, sequence uint64) uint64 { + return (clusterID << clusterIDShift) | + (apiserverID << apiserverIDShift) | + (timestamp << timestampShift) | + (tag << tagShift) | + sequence +} + +var ( + // regexJobID matches "{int}-{int}-{int}-{int}" — mirrors REGEX_JOBID in config.h. + regexJobID = regexp.MustCompile(`^\s*(\d+)-(\d+)-(\d+)-(\d+)\s*$`) + // regexJobArrayID matches "{int}-{int}-{int}-{int}[{int}]" — mirrors REGEX_JOBARRAY_ID in config.h. + regexJobArrayID = regexp.MustCompile(`^\s*(\d+)-(\d+)-(\d+)-(\d+)\[(\d+)\]\s*$`) +) + +// ParseJobID parses a human-readable job ID string into a packed 64-bit job ID, +// mirroring parse_jobid() in src/lib/utils.c:482. +// Accepts: "0-0-12874529-0" (regular) or "0-0-12874529-0[5]" (array child). +func ParseJobID(s string) (uint64, error) { + if m := regexJobArrayID.FindStringSubmatch(s); m != nil { + clusterID, _ := strconv.ParseUint(m[1], 10, 64) + apiserverID, _ := strconv.ParseUint(m[2], 10, 64) + timestamp, _ := strconv.ParseUint(m[3], 10, 64) + idJob, _ := strconv.ParseUint(m[4], 10, 64) + idIdx, _ := strconv.ParseUint(m[5], 10, 64) + return MakeJobID(clusterID, apiserverID, timestamp, tagJobArray, idJob+idIdx), nil + } + if m := regexJobID.FindStringSubmatch(s); m != nil { + clusterID, _ := strconv.ParseUint(m[1], 10, 64) + apiserverID, _ := strconv.ParseUint(m[2], 10, 64) + timestamp, _ := strconv.ParseUint(m[3], 10, 64) + idJob, _ := strconv.ParseUint(m[4], 10, 64) + return MakeJobID(clusterID, apiserverID, timestamp, 0, idJob), nil + } + return 0, fmt.Errorf("invalid job ID format: %q", s) +} + +// ExtractBaseJobID clears the tag and array_idx bits, returning the base job ID +// that the API server stores in the job_id column. +func ExtractBaseJobID(packed uint64) uint64 { + id := packed &^ (uint64((1<, " diff --git a/src/frontend/xcecli/filters/job_filter.go b/src/frontend/xcecli/filters/job_filter.go index 43d42abc..d7f53984 100644 --- a/src/frontend/xcecli/filters/job_filter.go +++ b/src/frontend/xcecli/filters/job_filter.go @@ -4,6 +4,8 @@ import ( "fmt" "regexp" "strings" + + "xcecli/common/format" ) // JobsFilter struct for filtering jobs @@ -39,12 +41,18 @@ func (f *JobsFilter) HandleJobFilter(array_type string) (string, error) { if f.JobIDs != "" { jobIdStr := strings.Split(f.JobIDs, ",") + apiIDs := make([]string, 0, len(jobIdStr)) for _, jobId := range jobIdStr { if err := isValidJobId(jobId); err != nil { return "", err } + apiID, err := format.ToAPIJobID(strings.TrimSpace(jobId)) + if err != nil { + return "", err + } + apiIDs = append(apiIDs, apiID) } - filters = append(filters, fmt.Sprintf("{job_id:in:(%s)}", f.JobIDs)) + filters = append(filters, fmt.Sprintf("{job_id:in:(%s)}", strings.Join(apiIDs, ","))) return strings.Join(filters, ","), nil } else { if array_type == "" { @@ -163,13 +171,20 @@ func GetJobStatusSet(f *JobsFilter) []string { return result } +var ( + reFmtJobID = regexp.MustCompile(`^\d+-\d+-\d+-\d+$`) + reFmtJobArrayID = regexp.MustCompile(`^\d+-\d+-\d+-\d+\[\d+\]$`) + reNumericID = regexp.MustCompile(`^\d+$`) + reNumericIndex = regexp.MustCompile(`^\d+\[\d+\]$`) +) + func isValidJobId(jobID string) error { - matched, _ := regexp.MatchString(`^\d+$`, jobID) - matchedIndex, _ := regexp.MatchString(`^\d+\[\d+\]$`, jobID) - if !matched && !matchedIndex { - return fmt.Errorf("jobid '%s' contains invalid characters. Allowed characters are 0-9 or with index value", jobID) + s := strings.TrimSpace(jobID) + if reFmtJobID.MatchString(s) || reFmtJobArrayID.MatchString(s) || + reNumericID.MatchString(s) || reNumericIndex.MatchString(s) { + return nil } - return nil + return fmt.Errorf("jobid '%s': must be numeric, numeric[idx], or cluster-apiserver-timestamp-seq[idx] format", jobID) } // Helper function, generates search rule