myxce/src/frontend/xcecli/models/jobs.go
Rongsong Shen ce2edf0dac refactor: represent time fields as int64 Unix seconds throughout the stack
Collapses a four-step conversion chain (time_t → int64 → time.Time →
RFC3339 string → string → time.Time) to a direct int64 pass-through:

  C time_t → rawJobData int64 → JobInfo int64 → JSON integer → QueryJob int64 → time.Unix() for display

Changes:
- apiserver/models/jobs.go: SubmitTime, QueueTime, StartTime, EndTime,
  ReserveTime in JobInfo and JobAdditionalFields: time.Time → int64
- apiserver/models/common.go: ActionControl.SubmitTime: time.Time → int64
- backend/convert_pure.go: remove time.Unix() calls; assign int64 directly
- backend/convert_test.go: update time assertions to compare int64 values
- xcecli/models/jobs.go: SubmitTime, StartTime, EndTime, ReserveTime in
  QueryJob: string/time.Time → int64; remove time import
- xcecli/common/output/time.go: replace parseJobTime(string) with
  unixToTime(int64) and fmtUnixTime(int64) helpers
- xcecli/common/output/page_print_jobs.go: replace parseJobTime() calls
  with unixToTime()/fmtUnixTime(); remove !startTime.IsZero() guard
  (unixToTime returns false for ts<=0, making it redundant)
- xcecli/common/output/page_print_array_elements_jobs.go: replace
  string-split+time.Parse submit-time block with fmtUnixTime(); remove
  unused time import

Side effect: eliminates the "0001-01-01T00:00:00Z" zero-time
serialization issue at the source — zero timestamps are now 0 (integer)
and unixToTime/fmtUnixTime return false/"-" for ts<=0 without special-
casing needed in callers.
2026-05-09 07:00:49 +00:00

524 lines
18 KiB
Go
Executable file

package models
import (
"encoding/json"
"fmt"
)
const (
HEADER_JOB_JOBID = "JOBID"
HEADER_JOB_USER = "USER"
HEADER_JOB_STATUS = "STATUS"
HEADER_JOB_QUEUE = "QUEUE"
HEADER_JOB_FROM_HOST = "FROM_HOST"
HEADER_JOB_EXEC_HOST = "EXEC_HOST"
HEADER_JOB_COMMAND = "COMMAND"
HEADER_JOB_NAME = "JOB_NAME"
HEADER_JOB_SUBMIT_TIME = "SUBMIT_TIME"
HEADER_JOB_CORE_LIMIT = "CORE_LIMIT"
HEADER_JOB_MEM_LIMIT = "MEM_LIMIT"
HEADER_JOB_PID = "PID"
HEADER_JOB_PGID = "PGID"
HEADER_JOB_PROJECT_NAME = "PROJECT_NAME"
HEADER_JOB_START_TIME = "START_TIME"
HEADER_JOB_END_TIME = "END_TIME"
HEADER_JOB_CWD = "CWD"
HEADER_JOB_CPU_TIME = "CPU_TIME"
HEADER_JOB_RESREQ = "RESREQ"
//HEADER_JOB_PRIORITY = "PRIORITY"
HEADER_JOB_DESCRIPTION = "DESCRIPTION"
HEADER_JOB_LAUNCH_TYPE = "LAUNCH_TYPE"
HEADER_JOB_NUM_PROCESSORS = "NUM_PROCESSORS"
HEADER_JOB_INTERACTIVE_URL = "INTERACTIVE_URL"
HEADER_JOB_PENDING_REASON = "PENDING_REASON"
// second line
HEADER_JOB_R15S = "r15s"
HEADER_JOB_R1M = "r1m"
HEADER_JOB_R15M = "r15m"
HEADER_JOB_UT = "ut"
HEADER_JOB_PG = "pg"
HEADER_JOB_IO = "io"
HEADER_JOB_LS = "ls"
HEADER_JOB_IT = "it"
HEADER_JOB_TMP = "tmp"
HEADER_JOB_SWP = "swp"
HEADER_JOB_MEM = "mem"
)
var JobLoadSchedulingHeaders = []string{
HEADER_JOB_R15S,
HEADER_JOB_R1M,
HEADER_JOB_R15M,
HEADER_JOB_UT,
HEADER_JOB_PG,
HEADER_JOB_IO,
HEADER_JOB_LS,
HEADER_JOB_IT,
HEADER_JOB_TMP,
HEADER_JOB_SWP,
HEADER_JOB_MEM,
}
// for array jobs
const (
HEADER_ARRAY_JOB_JOBID = "JOBID"
HEADER_ARRAY_JOB_SPEC = "ARRAY_SPEC"
HEADER_ARRAY_JOB_OWNER = "OWNER"
HEADER_ARRAY_JOB_NJOBS = "NJOBS"
HEADER_ARRAY_JOB_PEND = "PEND"
HEADER_ARRAY_JOB_DONE = "DONE"
HEADER_ARRAY_JOB_RUN = "RUN"
HEADER_ARRAY_JOB_EXIT = "EXIT"
HEADER_ARRAY_JOB_SSUSP = "SSUSP"
HEADER_ARRAY_JOB_USUSP = "USUSP"
HEADER_ARRAY_JOB_PSUSP = "PSUSP"
)
type JobsResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data struct {
Count int `json:"count"`
Rows []QueryJob `json:"rows"`
Page_token string `json:"page_token"`
} `json:"data"`
}
type Jsonb struct {
String string
}
// Job define the data structure in the job info
type QueryJob struct {
JobID int64 `json:"job_id"`
SubmitUser string `json:"submit_user"`
JobStatus string `json:"job_status"`
JobName string `json:"job_name"`
Command string `json:"command"`
QueueName string `json:"queue_name"`
ProjectName string `json:"project_name"`
UserGroup string `json:"user_group"`
LicenseProject string `json:"license_project"`
JobDescription string `json:"job_description"`
Resreq string `json:"resreq"`
RsvID string `json:"rsv_id"`
SLA string `json:"sla"`
MaxNumProcessors int `json:"maxnum_processors"`
NumProcessors int `json:"num_processors"`
AskedHosts string `json:"asked_hosts"`
BeginTime string `json:"begin_time"`
TermTime string `json:"term_time"`
UserPriority int `json:"user_priority"`
DependCond string `json:"depend_cond"`
SubmitTime int64 `json:"submit_time"`
ReserveTime int64 `json:"reserve_time"`
StartTime int64 `json:"start_time"`
EndTime int64 `json:"end_time"`
JobRusage string `json:"job_rusage"`
MaxMem float64 `json:"max_mem"`
AvgMem float64 `json:"avg_mem"`
UsedMem float64 `json:"used_mem"`
CPUTime int `json:"cpu_time"`
PeakCPUTime int `json:"peak_cpu_time"`
PeakCPUDuration float64 `json:"peak_cpu_duration"`
MemEfficiency float64 `json:"mem_efficiency"` // Memory efficiency in percentage
AvgCPUEfficiency float64 `json:"avg_cpu_efficiency"` // Average CPU efficiency in percentage
PeakCPUEfficiency float64 `json:"peak_cpu_efficiency"` // Peak CPU efficiency in percentage
NumHosts int `json:"num_hosts"` // Number of hosts used by the job
FirstHost string `json:"first_host"` // First host where the job ran
AllocSlot string `json:"alloc_slots"` // Allocated slot information
Duration int `json:"duration"`
Umask int `json:"umask"`
CWD string `json:"cwd"`
SubmitHomeDir string `json:"submit_home_dir"`
FromHost string `json:"from_host"`
ExecHosts ExecHosts `json:"exec_hosts"`
ExitStatus ExitStatusValue `json:"exit_status"`
ExecUID int `json:"exec_uid"`
ExecHome string `json:"exec_home"`
ExecCWD string `json:"exec_cwd"`
ExecUserName string `json:"exec_user_name"`
JobPriority int `json:"job_priority"`
ChargedSAAP string `json:"charged_SAAP"`
PreExecCmd string `json:"pre_exec_cmd"`
PostExecCmd string `json:"post_exec_cmd"`
RequeueErrorValues string `json:"requeue_error_values"`
NumLicenses int `json:"num_licenses"`
LicenseNames []string `json:"license_names"`
RunTime int `json:"run_time"`
RunTimeEstimation int `json:"run_time_estimation"`
MigThreshold int `json:"mig_threshold"`
ChkpntPeriod int `json:"chkpnt_period"`
ChkpntDir string `json:"chkpnt_dir"`
OutDir string `json:"out_dir"`
InFile string `json:"in_file"`
OutFile string `json:"out_file"`
ErrFile string `json:"err_file"`
MailUser string `json:"mail_user"`
LoginShell string `json:"login_shell"`
ExceptList string `json:"except_list"`
RLimits64 []int64 `json:"rlimits64"`
SubEnvVars string `json:"sub_env_vars"`
PendingReason string `json:"pending_reason"`
JobStartInfo string `json:"job_start_info"`
JobExecInfo string `json:"job_exec_info"`
CoreLimit float64 `json:"core_limit"`
CPULimit string `json:"cpu_limit"`
RunLimit string `json:"run_limit"`
FileLimit float64 `json:"file_limit"`
DataLimit float64 `json:"data_limit"`
StackLimit float64 `json:"stack_limit"`
MemLimit float64 `json:"mem_limit"`
SwapLimit float64 `json:"swap_limit"`
ProcessLimit float64 `json:"process_limit"`
ThreadLimit float64 `json:"thread_limit"`
NumFiles int `json:"num_files"`
ArrayIdx int `json:"array_idx"` //
UniqueJobID int64 `json:"unique_job_id"` //
ExtendData JobExtendData `json:"extend_data"`
InteractiveJobURL string `json:"interactive_job_url"`
PGIDs IntSlice `json:"pgids"`
PID IntSlice `json:"pids"`
LaunchType string `json:"launch_type"`
}
var JobFieldsMap = map[string]string{
HEADER_JOB_JOBID: "job_id",
HEADER_JOB_USER: "submit_user",
HEADER_JOB_STATUS: "job_status",
HEADER_JOB_QUEUE: "queue_name",
HEADER_JOB_COMMAND: "command",
HEADER_JOB_NAME: "job_name",
HEADER_JOB_SUBMIT_TIME: "submit_time",
HEADER_JOB_FROM_HOST: "from_host",
HEADER_JOB_EXEC_HOST: "exec_hosts",
HEADER_JOB_CORE_LIMIT: "core_limit",
HEADER_JOB_MEM_LIMIT: "mem_limit",
HEADER_JOB_PID: "pids",
HEADER_JOB_PGID: "pgids",
HEADER_JOB_PROJECT_NAME: "project_name",
HEADER_JOB_START_TIME: "start_time",
HEADER_JOB_END_TIME: "end_time",
HEADER_JOB_CWD: "cwd",
HEADER_JOB_CPU_TIME: "cpu_time",
HEADER_JOB_RESREQ: "resreq",
HEADER_JOB_DESCRIPTION: "job_description",
HEADER_JOB_LAUNCH_TYPE: "launch_type",
HEADER_JOB_NUM_PROCESSORS: "num_processors",
HEADER_JOB_INTERACTIVE_URL: "interactive_job_url",
HEADER_JOB_PENDING_REASON: "pending_reason",
//job_filter.HEADER_JOB_PRIORITY: "job_priority",
}
type IntSlice []int
func (s *IntSlice) UnmarshalJSON(data []byte) error {
// parse array
var arr []int
if err := json.Unmarshal(data, &arr); err == nil {
*s = arr
return nil
}
// parse {}
var obj map[string]interface{}
if err := json.Unmarshal(data, &obj); err == nil {
if len(obj) == 0 {
*s = []int{}
return nil
}
}
return fmt.Errorf("cannot unmarshal IntSlice %s", string(data))
}
// ExecHosts represents the execution hosts for a job
type ExecHosts struct {
HostSlots []HostSlot `json:"host_slots"`
NumHosts int `json:"num_hosts"`
}
// String returns the string representation of ExecHosts
// If HostSlots is empty, return "-"
// Otherwise, return the JSON formatted string
func (e ExecHosts) String() string {
if len(e.HostSlots) == 0 {
return "-"
}
// Use json.Marshal to convert the structure to a JSON string
data, err := json.Marshal(e)
if err != nil {
return "-"
}
return string(data)
}
// UnmarshalJSON customizes the JSON parsing method, compatible with both string "-" and structure formats
func (e *ExecHosts) UnmarshalJSON(data []byte) error {
// JSON null means no allocation — treat as empty
if string(data) == "null" {
e.HostSlots = []HostSlot{}
e.NumHosts = 0
return nil
}
// First try to parse as a string
var str string
if err := json.Unmarshal(data, &str); err == nil {
// Empty string or "-" both mean no hosts
if str == "" || str == "-" {
e.HostSlots = []HostSlot{}
e.NumHosts = 0
return nil
}
// Any other non-empty string is a single hostname
e.HostSlots = []HostSlot{{HostName: str, NumSlots: 1}}
e.NumHosts = 1
return nil
}
// If it's not a string, try to parse as an ExecHosts structure
type Alias ExecHosts
aux := &struct {
*Alias
}{
Alias: (*Alias)(e),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
return nil
}
// HostSlot represents a single host slot
type HostSlot struct {
HostName string `json:"host_name"`
NumSlots int `json:"num_slots"`
}
type JobSubmitRequest struct {
JobName string `json:"job_name,omitempty" binding:"required"`
Command string `json:"command" binding:"required"`
QueueName string `json:"queue_name,omitempty"` // set to DEFAULT_QUEUE in config
ProjectName string `json:"project_name,omitempty"` // default project: "default"
UserGroup string `json:"user_group,omitempty"`
LicenseProject string `json:"license_project,omitempty"`
JobDescription string `json:"job_description,omitempty"`
Resreq string `json:"resreq,omitempty"` // default: "type == local"
Gpureq string `json:"gpureq,omitempty"`
RsvID string `json:"rsv_id,omitempty"`
SLA string `json:"sla,omitempty"`
MaxNumProcessors int `json:"maxnum_processors,omitempty"`
NumProcessors int `json:"num_processors,omitempty"`
AskedHosts string `json:"asked_hosts,omitempty"`
BeginTime string `json:"begin_time,omitempty"`
TermTime string `json:"term_time,omitempty"`
UserPriority int `json:"user_priority,omitempty"`
DependCond string `json:"depend_cond,omitempty"`
JobGroup string `json:"job_group,omitempty"`
HostSpec string `json:"host_spec,omitempty"`
Env []string `json:"env,omitempty"`
Tags []string `json:"tags,omitempty"`
Exclusive bool `json:"exclusive,omitempty"`
Rerunable bool `json:"rerunable,omitempty"`
Interactive int `json:"interactive,omitempty"`
WarningTimePeriod int `json:"warning_time_period,omitempty"`
WarningAction string `json:"warning_action,omitempty"`
DataSpecFile string `json:"data_spec_file,omitempty"`
TransferFiles []string `json:"transfer_files,omitempty"`
PreExecCmd string `json:"pre_exec_cmd,omitempty"`
PostExecCmd string `json:"post_exec_cmd,omitempty"`
RequeueErrorValues string `json:"requeue_error_values,omitempty"`
RunTimeEstimation int `json:"run_time_estimation,omitempty"`
MigThreshold int `json:"mig_threshold,omitempty"`
ChkpntPeriod int `json:"chkpnt_period,omitempty"`
ChkpntDir string `json:"chkpnt_dir,omitempty"`
InitChkpntPeriod int `json:"init_chkpnt_period,omitempty"`
CWD string `json:"cwd,omitempty"`
OutDir string `json:"out_dir,omitempty"`
InFile string `json:"in_file,omitempty"`
OutFile string `json:"out_file,omitempty"`
ErrFile string `json:"err_file,omitempty"`
Mailuser string `json:"mailuser,omitempty"`
LoginShell string `json:"login_shell,omitempty"`
ExceptList string `json:"except_list,omitempty"`
CPULimit string `json:"cpu_limit,omitempty"`
RunLimit string `json:"run_limit,omitempty"`
MemLimit string `json:"mem_limit,omitempty"`
SwapLimit string `json:"swap_limit,omitempty"`
CoreLimit string `json:"core_limit,omitempty"`
DataLimit string `json:"data_limit,omitempty"`
FileLimit string `json:"file_limit,omitempty"`
ProcessLimit string `json:"process_limit,omitempty"`
StackLimit string `json:"stack_limit,omitempty"`
ThreadLimit string `json:"thread_limit,omitempty"`
ShellLimit string `json:"shell_limit,omitempty"`
NumFiles int `json:"num_files,omitempty"`
LaunchType string `json:"launch_type,omitempty"`
}
// JobSubmitData define the data structure in the job submit response
type JobSubmitData struct {
JobID int64 `json:"jobid"`
Message string `json:"message"`
}
// JobSubmitResponse define the data structure in the job submit response
type JobSubmitResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data JobSubmitData `json:"data"`
Count int `json:"count"`
}
// UploadedFile represents the uploaded file
type UploadedFile struct {
DestFile string `json:"dest_file"` // dest file path
SrcFile string `json:"src_file"` // src file name
WebURL string `json:"web_url"` // file URL
}
// UploadResponse represents the file upload response
type UploadResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data struct {
UploadedFiles []UploadedFile `json:"accepted"`
Failed []FailedFile `json:"failed"`
Status string `json:"status"`
} `json:"data"`
}
type FailedFile struct {
FileName string `json:"file_name"`
Msg string `json:"msg"`
}
// ------- job control -------------
type JobControlFilter struct {
JobName string `json:"job_name,omitempty"`
JobGroupName string `json:"job_group_name,omitempty"`
QueueName string `json:"queue_name,omitempty"`
Users string `json:"users,omitempty"`
ExecHosts string `json:"exec_hosts,omitempty"`
SLA string `json:"sla,omitempty"`
AllJobs bool `json:"all_jobs,omitempty"`
}
type JobControlRequest struct {
Filter JobControlFilter `json:"filter" binding:"required"`
Action string `json:"action" binding:"required"`
Signal string `json:"signal,omitempty"`
Comment string `json:"comment,omitempty"`
ForceKill bool `json:"force_kill,omitempty"`
}
type JobControlJobIDRequest struct {
Action string `json:"action" binding:"required"`
Signal string `json:"signal,omitempty"` // signal value
Comment string `json:"comment,omitempty"`
ForceKill bool `json:"force_kill,omitempty"`
}
type JobRestartRequest struct {
AskedHosts string `json:"asked_hosts,omitempty"`
}
type JobRestartResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data *JobRestartResponseData `json:"data"`
}
type JobRestartResponseData struct {
JobID int64 `json:"jobid"`
QueueName string `json:"queue_name"`
Message string `json:"message"`
}
type JobControlResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data *JobControlResponseData `json:"data"`
}
type JobControlResponseData struct {
Accepted []*JobControlResponseItem `json:"accepted"`
Failed []*JobControlResponseItem `json:"failed"`
Status string `json:"status"`
}
type JobControlResponseItem struct {
JobID int64 `json:"job_id"`
ArrayIdx int `json:"array_idx,omitempty"`
Msg string `json:"msg"`
}
// job array statistics data structure
type ArrayJobStatsData struct {
JobID int64 `json:"job_id"`
ArrayJobName string `json:"array_job_name"`
SubmitUser string `json:"submit_user"`
NumJobs int `json:"num_jobs"`
NumDone int `json:"num_done"`
NumExit int `json:"num_exit"`
NumRun int `json:"num_run"`
NumPend int `json:"num_pend"`
NumSSusp int `json:"num_ssusp"`
NumUSusp int `json:"num_ususp"`
NumPSusp int `json:"num_psusp"`
}
// job array statistics response data structure
type ArrayJobsStatsResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data struct {
Count int `json:"count"`
Rows []ArrayJobStatsData `json:"rows"`
Page_token string `json:"page_token"`
} `json:"data"`
}
type JobExtendData struct {
Interactive int `json:"interactive"`
InteractiveJobURL string `json:"interactive_job_url"`
Limits struct {
CoreLimit int `json:"core_limit"`
} `json:"limits"`
}
func (j *QueryJob) UnmarshalJSON(data []byte) error {
type Alias QueryJob
var a Alias
if err := UnmarshalOrderMapRow(data, &a); err != nil {
return err
}
*j = QueryJob(a)
return nil
}
// ExitStatusValue handles exit_status which can be null, a number, or a JSON string.
type ExitStatusValue string
func (e *ExitStatusValue) UnmarshalJSON(data []byte) error {
s := string(data)
if s == "null" || s == "" {
*e = ""
return nil
}
// strip quotes if it's a JSON string
if len(s) >= 2 && s[0] == '"' {
var str string
if err := json.Unmarshal(data, &str); err != nil {
return err
}
*e = ExitStatusValue(str)
return nil
}
// bare number or other value — store as-is
*e = ExitStatusValue(s)
return nil
}