fix(apiserver): fix job control union discriminant, add human-readable job ID support (gitea#45)

ControlJob() set xce_jobctrl_t.action to the job_ctrl_type_t action code
(kill/stop/resume/checkpoint), but that field is actually the capnp
JobControl_u_which union discriminant -- a different enum entirely. This
made kill/resume/checkpoint hit request.c's -EINVAL default case, and made
stop collide with the checkpoint discriminant and read through the wrong
union member. Live-verified against a real deployed cluster: kill/stop/
resume/checkpoint now behave correctly with no apiserver crash.

Also additively accept a human-readable job ID ("{cluster}-{apiserver}-
{timestamp}-{seq}") on the job control endpoint, alongside the existing
numeric form xcecli already translates to -- lets a caller hit the REST
API directly without reimplementing xcecli's ID packing. Live-verified via
a direct curl call with the human-readable ID.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hb6Edtj4KbS8CrgEWYdA2E
This commit is contained in:
Rongsong Shen 2026-09-07 08:31:54 +00:00
parent 5ab344e1ae
commit 099dbdabdc
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
5 changed files with 191 additions and 5 deletions

View file

@ -3,6 +3,8 @@ package backend
import (
"apiserver/models"
"encoding/json"
"regexp"
"strconv"
"time"
"fmt"
)
@ -98,12 +100,60 @@ func FormatJobID(uniqueJobID int64) string {
clusterID, apiserverID, timestamp, sequence)
}
// humanJobIDPattern matches a plain human-readable job ID, "{cluster}-
// {apiserver}-{timestamp}-{sequence}" (no array-index brackets).
var humanJobIDPattern = regexp.MustCompile(`^(\d+)-(\d+)-(\d+)-(\d+)$`)
// ParseHumanJobID parses a plain human-readable job ID (as produced by
// FormatJobID, brackets already stripped by the caller) back into the
// packed numeric job ID the job_id/unique_job_id DB columns store. Returns
// ok=false if s doesn't match that shape (e.g. it's already numeric).
//
// Deliberately does not itself handle an "[idx]" array-child suffix:
// controllers/jobs.go's parseArrayName already splits that off (for the
// API's own array-index filter syntax) before this ever runs, leaving a
// plain quad whose 4th number is the packed ID's low sequence bits with
// tag=0 -- true for a real non-array job's own ID, and also true for an
// array child's dash-quad prefix (FormatJobID's arrayBase is those same
// bits already shifted into position with tag/idx zeroed, i.e. exactly
// ExtractArrayBaseID(original)).
func ParseHumanJobID(s string) (uint64, bool) {
m := humanJobIDPattern.FindStringSubmatch(s)
if m == nil {
return 0, false
}
clusterID, _ := strconv.ParseUint(m[1], 10, 64)
apiserverID, _ := strconv.ParseUint(m[2], 10, 64)
timestamp, _ := strconv.ParseUint(m[3], 10, 64)
sequence, _ := strconv.ParseUint(m[4], 10, 64)
return (clusterID << clusterIDShift) |
(apiserverID << apiserverIDShift) |
(timestamp << timestampShift) |
sequence, true
}
// 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)
}
// jobCtrlUnionDiscriminant returns the value xce_jobctrl_t.action must carry
// for the given job_ctrl_type_t action (models.JOB_STOP/RESUME/KILL/CHKPNT):
// the capnp JobControl_u_which union discriminant (JobControl_u_siginfo=0,
// JobControl_u_ckptoption=1 -- src/lib/controller.capnp.h), NOT the action
// code itself. xce_jobctrl_req() (src/lib/request.c) switches on this field
// and rejects anything else with -EINVAL; ControlJob (clib.go) previously
// stored the raw action code there instead, so kill(3)/resume(2) always hit
// that -EINVAL default case (gitea issue #45) while stop(1) happened to
// collide with ckptoption(1) and read through the wrong union member.
func jobCtrlUnionDiscriminant(action int) int {
if action == models.JOB_CHKPNT {
return 1 // JobControl_u_ckptoption
}
return 0 // JobControl_u_siginfo (stop/resume/kill all signal the job)
}
// resolveExecUser returns the effective exec user for a job submission: the
// caller-specified ExecUser if present, otherwise the submitting user
// (bug-1780: an unset execuser in the startjob request reaches xcecd as