feat(job-array-one-based-index): apiserver backend ID helpers and array ID wrapper (task 5.1)

GenerateArrayJobID wraps libxce idgen_array (first element, index 1;
one-element arrays included) with a non-cgo stub; pure helpers
ParseHumanJobIDStrict (written identity with exact field-width checks,
uint64 overflow reported as a width error), IsArrayCapableKey and
MakeElementID (bit-identical to libxce jobsel_element_id).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-10-01 11:29:11 +00:00
parent 47b9938120
commit b88e352616
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
7 changed files with 260 additions and 3 deletions

View file

@ -3,10 +3,11 @@ package backend
import (
"apiserver/models"
"encoding/json"
"errors"
"fmt"
"regexp"
"strconv"
"time"
"fmt"
)
// JobID bit layout constants — mirrors src/include/config.h.
@ -132,6 +133,66 @@ func ParseHumanJobID(s string) (uint64, bool) {
sequence, true
}
// Errors returned by ParseHumanJobIDStrict. Callers map them to their own
// messages: ErrJobIDSyntax is the written-form syntax rule, ErrJobIDWidth the
// field-width rule (job-array-one-based-index 5.14).
var (
ErrJobIDSyntax = errors.New("job ID is not of the form cluster-apiserver-timestamp-sequence")
ErrJobIDWidth = errors.New("job ID field out of range")
)
// ParseHumanJobIDStrict parses a written job identity "c-a-ts-seq" (decimal
// digits only, leading zeros allowed, no sign, no whitespace, no "[...]")
// into its packed, untagged ID. Unlike ParseHumanJobID it checks every field
// against its bit width -- cluster <= 63, apiserver <= 15, timestamp < 2^32,
// sequence < 2^20 -- so an oversized field is rejected (ErrJobIDWidth)
// instead of spilling into a neighbouring field. A field too large even for
// uint64 is also ErrJobIDWidth. Anything not of that shape is ErrJobIDSyntax.
func ParseHumanJobIDStrict(s string) (uint64, error) {
m := humanJobIDPattern.FindStringSubmatch(s)
if m == nil {
return 0, ErrJobIDSyntax
}
widths := [4]int{nClusterIDBits, nApiserverIDBits, nTimestampBits, nSequenceBits}
var f [4]uint64
for i, w := range widths {
v, err := strconv.ParseUint(m[i+1], 10, 64)
if err != nil {
if errors.Is(err, strconv.ErrRange) {
return 0, ErrJobIDWidth
}
return 0, ErrJobIDSyntax
}
if v >= uint64(1)<<w {
return 0, ErrJobIDWidth
}
f[i] = v
}
return (f[0] << clusterIDShift) |
(f[1] << apiserverIDShift) |
(f[2] << timestampShift) |
f[3], nil
}
// arrayIdxMask covers the 15-bit array index field (bits 0-14).
const arrayIdxMask = uint64(1)<<nArrayIdxBits - 1
// IsArrayCapableKey reports whether key can name an array (an "<array-id>"
// in "<array-id>[k]"): tag 0 and the low 15 (index) bits clear, i.e. the
// sequence is a multiple of 32768 (job-array-one-based-index 5.8).
func IsArrayCapableKey(key uint64) bool {
return extractIDTag(key) == 0 && key&arrayIdxMask == 0
}
// MakeElementID composes the ID of element k of the array identified by key:
// key's cluster, apiserver, timestamp and sequence-high bits, tag
// TAG_JOB_ARRAY and index k (masked to 15 bits). key's own tag and index bits
// are ignored. Mirrors jobsel_element_id() in src/lib/utils.c.
func MakeElementID(key uint64, k int) uint64 {
base := key &^ (uint64(1<<nTagBits-1)<<tagShift | arrayIdxMask)
return uint64(tagJobArray)<<tagShift | base | uint64(k)&arrayIdxMask
}
// 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 {