From d46a784efb8673c3b28ead33144fb121602874cf Mon Sep 17 00:00:00 2001 From: dailz Date: Wed, 15 Apr 2026 21:30:44 +0800 Subject: [PATCH] feat(model): add Task model, DTOs, and status constants for task submission system --- internal/model/task.go | 93 ++++++++++++++++++++++++++++++++ internal/model/task_test.go | 104 ++++++++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 internal/model/task.go create mode 100644 internal/model/task_test.go diff --git a/internal/model/task.go b/internal/model/task.go new file mode 100644 index 0000000..36aaa78 --- /dev/null +++ b/internal/model/task.go @@ -0,0 +1,93 @@ +package model + +import ( + "encoding/json" + "time" + + "gorm.io/gorm" +) + +// Task status constants. +const ( + TaskStatusSubmitted = "submitted" + TaskStatusPreparing = "preparing" + TaskStatusDownloading = "downloading" + TaskStatusReady = "ready" + TaskStatusQueued = "queued" + TaskStatusRunning = "running" + TaskStatusCompleted = "completed" + TaskStatusFailed = "failed" +) + +// Task step constants for step-level retry tracking. +const ( + TaskStepPreparing = "preparing" + TaskStepDownloading = "downloading" + TaskStepSubmitting = "submitting" +) + +// Task represents an HPC task submitted through the application framework. +type Task struct { + ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` + TaskName string `gorm:"size:255" json:"task_name"` + AppID int64 `json:"app_id"` + AppName string `gorm:"size:255" json:"app_name"` + Status string `json:"status"` + CurrentStep string `json:"current_step"` + RetryCount int `json:"retry_count"` + Values json.RawMessage `gorm:"type:text" json:"values,omitempty"` + InputFileIDs json.RawMessage `json:"input_file_ids" gorm:"column:input_file_ids;type:text"` + Script string `json:"script,omitempty"` + SlurmJobID *int32 `json:"slurm_job_id,omitempty"` + WorkDir string `json:"work_dir,omitempty"` + Partition string `json:"partition,omitempty"` + ErrorMessage string `json:"error_message,omitempty"` + UserID string `json:"user_id"` + SubmittedAt time.Time `json:"submitted_at"` + StartedAt *time.Time `json:"started_at,omitempty"` + FinishedAt *time.Time `json:"finished_at,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"` +} + +func (Task) TableName() string { + return "hpc_tasks" +} + +// CreateTaskRequest is the DTO for creating a new task. +type CreateTaskRequest struct { + AppID int64 `json:"app_id" binding:"required"` + TaskName string `json:"task_name"` + Values map[string]string `json:"values"` + InputFileIDs []int64 `json:"file_ids"` +} + +// TaskResponse is the DTO returned in API responses. +type TaskResponse struct { + ID int64 `json:"id"` + TaskName string `json:"task_name"` + AppID int64 `json:"app_id"` + AppName string `json:"app_name"` + Status string `json:"status"` + CurrentStep string `json:"current_step"` + RetryCount int `json:"retry_count"` + SlurmJobID *int32 `json:"slurm_job_id"` + WorkDir string `json:"work_dir"` + ErrorMessage string `json:"error_message"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// TaskListResponse is the paginated response for listing tasks. +type TaskListResponse struct { + Items []TaskResponse `json:"items"` + Total int64 `json:"total"` +} + +// TaskListQuery contains query parameters for listing tasks. +type TaskListQuery struct { + Page int `form:"page" json:"page,omitempty"` + PageSize int `form:"page_size" json:"page_size,omitempty"` + Status string `form:"status" json:"status,omitempty"` +} diff --git a/internal/model/task_test.go b/internal/model/task_test.go new file mode 100644 index 0000000..bd3882f --- /dev/null +++ b/internal/model/task_test.go @@ -0,0 +1,104 @@ +package model + +import ( + "encoding/json" + "testing" + "time" +) + +func TestTask_TableName(t *testing.T) { + task := Task{} + if got := task.TableName(); got != "hpc_tasks" { + t.Errorf("Task.TableName() = %q, want %q", got, "hpc_tasks") + } +} + +func TestTask_JSONRoundTrip(t *testing.T) { + now := time.Now().UTC().Truncate(time.Second) + jobID := int32(42) + + task := Task{ + ID: 1, + TaskName: "test task", + AppID: 10, + AppName: "GROMACS", + Status: TaskStatusRunning, + CurrentStep: TaskStepSubmitting, + RetryCount: 1, + Values: json.RawMessage(`{"np":"4"}`), + InputFileIDs: json.RawMessage(`[1,2,3]`), + Script: "#!/bin/bash", + SlurmJobID: &jobID, + WorkDir: "/data/work", + Partition: "gpu", + ErrorMessage: "", + UserID: "user1", + SubmittedAt: now, + StartedAt: &now, + FinishedAt: nil, + CreatedAt: now, + UpdatedAt: now, + } + + data, err := json.Marshal(task) + if err != nil { + t.Fatalf("marshal task: %v", err) + } + + var got Task + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("unmarshal task: %v", err) + } + + if got.ID != task.ID { + t.Errorf("ID = %d, want %d", got.ID, task.ID) + } + if got.TaskName != task.TaskName { + t.Errorf("TaskName = %q, want %q", got.TaskName, task.TaskName) + } + if got.Status != task.Status { + t.Errorf("Status = %q, want %q", got.Status, task.Status) + } + if got.CurrentStep != task.CurrentStep { + t.Errorf("CurrentStep = %q, want %q", got.CurrentStep, task.CurrentStep) + } + if got.RetryCount != task.RetryCount { + t.Errorf("RetryCount = %d, want %d", got.RetryCount, task.RetryCount) + } + if got.SlurmJobID == nil || *got.SlurmJobID != jobID { + t.Errorf("SlurmJobID = %v, want %d", got.SlurmJobID, jobID) + } + if got.UserID != task.UserID { + t.Errorf("UserID = %q, want %q", got.UserID, task.UserID) + } + if string(got.Values) != string(task.Values) { + t.Errorf("Values = %s, want %s", got.Values, task.Values) + } + if string(got.InputFileIDs) != string(task.InputFileIDs) { + t.Errorf("InputFileIDs = %s, want %s", got.InputFileIDs, task.InputFileIDs) + } + if got.FinishedAt != nil { + t.Errorf("FinishedAt = %v, want nil", got.FinishedAt) + } +} + +func TestCreateTaskRequest_JSONBinding(t *testing.T) { + payload := `{"app_id":5,"task_name":"my task","values":{"np":"8"},"file_ids":[10,20]}` + var req CreateTaskRequest + if err := json.Unmarshal([]byte(payload), &req); err != nil { + t.Fatalf("unmarshal CreateTaskRequest: %v", err) + } + + if req.AppID != 5 { + t.Errorf("AppID = %d, want 5", req.AppID) + } + if req.TaskName != "my task" { + t.Errorf("TaskName = %q, want %q", req.TaskName, "my task") + } + if v, ok := req.Values["np"]; !ok || v != "8" { + t.Errorf("Values[\"np\"] = %q, want %q", v, "8") + } + if len(req.InputFileIDs) != 2 || req.InputFileIDs[0] != 10 || req.InputFileIDs[1] != 20 { + t.Errorf("InputFileIDs = %v, want [10 20]", req.InputFileIDs) + } +}