diff --git a/internal/handler/job.go b/internal/handler/job.go index cb2e5bf..18c8c74 100644 --- a/internal/handler/job.go +++ b/internal/handler/job.go @@ -46,16 +46,30 @@ func (h *JobHandler) SubmitJob(c *gin.Context) { server.Created(c, resp) } -// GetJobs handles GET /api/v1/jobs. +// GetJobs handles GET /api/v1/jobs with pagination. func (h *JobHandler) GetJobs(c *gin.Context) { - jobs, err := h.jobSvc.GetJobs(c.Request.Context()) + var query model.JobListQuery + if err := c.ShouldBindQuery(&query); err != nil { + h.logger.Warn("bad request", zap.String("method", "GetJobs"), zap.String("error", "invalid query params")) + server.BadRequest(c, "invalid query params") + return + } + + if query.Page < 1 { + query.Page = 1 + } + if query.PageSize < 1 { + query.PageSize = 20 + } + + resp, err := h.jobSvc.GetJobs(c.Request.Context(), &query) if err != nil { h.logger.Error("handler error", zap.String("method", "GetJobs"), zap.Int("status", http.StatusInternalServerError), zap.Error(err)) server.InternalError(c, err.Error()) return } - server.OK(c, jobs) + server.OK(c, resp) } // GetJob handles GET /api/v1/jobs/:id. diff --git a/internal/handler/job_test.go b/internal/handler/job_test.go index edae13c..a33689d 100644 --- a/internal/handler/job_test.go +++ b/internal/handler/job_test.go @@ -188,7 +188,7 @@ func TestGetJobs_Success(t *testing.T) { router := setupJobRouter(handler) - req := httptest.NewRequest(http.MethodGet, "/api/v1/jobs", nil) + req := httptest.NewRequest(http.MethodGet, "/api/v1/jobs?page=1&page_size=10", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) @@ -202,6 +202,93 @@ func TestGetJobs_Success(t *testing.T) { if !resp["success"].(bool) { t.Fatal("expected success=true") } + data := resp["data"].(map[string]interface{}) + jobs := data["jobs"].([]interface{}) + if len(jobs) != 2 { + t.Fatalf("expected 2 jobs, got %d", len(jobs)) + } + if int(data["total"].(float64)) != 2 { + t.Errorf("expected total=2, got %v", data["total"]) + } + if int(data["page"].(float64)) != 1 { + t.Errorf("expected page=1, got %v", data["page"]) + } + if int(data["page_size"].(float64)) != 10 { + t.Errorf("expected page_size=10, got %v", data["page_size"]) + } +} + +func TestGetJobs_Pagination(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurm/v0.0.40/jobs", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(slurm.OpenapiJobInfoResp{ + Jobs: []slurm.JobInfo{ + {JobID: slurm.Ptr(int32(1)), Name: slurm.Ptr("job1")}, + {JobID: slurm.Ptr(int32(2)), Name: slurm.Ptr("job2")}, + {JobID: slurm.Ptr(int32(3)), Name: slurm.Ptr("job3")}, + }, + }) + }) + srv, handler := setupJobHandler(mux) + defer srv.Close() + + router := setupJobRouter(handler) + + req := httptest.NewRequest(http.MethodGet, "/api/v1/jobs?page=2&page_size=1", nil) + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + var resp map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &resp) + data := resp["data"].(map[string]interface{}) + jobs := data["jobs"].([]interface{}) + if len(jobs) != 1 { + t.Fatalf("expected 1 job on page 2, got %d", len(jobs)) + } + if int(data["total"].(float64)) != 3 { + t.Errorf("expected total=3, got %v", data["total"]) + } + jobData := jobs[0].(map[string]interface{}) + if int(jobData["job_id"].(float64)) != 2 { + t.Errorf("expected job_id=2 on page 2, got %v", jobData["job_id"]) + } +} + +func TestGetJobs_DefaultPagination(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurm/v0.0.40/jobs", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(slurm.OpenapiJobInfoResp{Jobs: []slurm.JobInfo{}}) + }) + srv, handler := setupJobHandler(mux) + defer srv.Close() + + router := setupJobRouter(handler) + + req := httptest.NewRequest(http.MethodGet, "/api/v1/jobs", nil) + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + var resp map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &resp) + data := resp["data"].(map[string]interface{}) + if int(data["page"].(float64)) != 1 { + t.Errorf("expected default page=1, got %v", data["page"]) + } + if int(data["page_size"].(float64)) != 20 { + t.Errorf("expected default page_size=20, got %v", data["page_size"]) + } } func TestGetJob_Success(t *testing.T) { diff --git a/internal/model/job.go b/internal/model/job.go index 43a7261..d9b5268 100644 --- a/internal/model/job.go +++ b/internal/model/job.go @@ -66,6 +66,12 @@ type JobListResponse struct { PageSize int `json:"page_size"` // 每页条数 } +// JobListQuery contains pagination parameters for active job listing. +type JobListQuery struct { + Page int `form:"page,default=1" json:"page,omitempty"` // 页码 (从 1 开始) + PageSize int `form:"page_size,default=20" json:"page_size,omitempty"` // 每页条数 +} + // JobHistoryQuery contains query parameters for job history. type JobHistoryQuery struct { Users string `form:"users" json:"users,omitempty"` // 按用户名过滤 (逗号分隔) diff --git a/internal/service/job_service.go b/internal/service/job_service.go index 0454058..ce84f47 100644 --- a/internal/service/job_service.go +++ b/internal/service/job_service.go @@ -82,8 +82,8 @@ func (s *JobService) SubmitJob(ctx context.Context, req *model.SubmitJobRequest) return resp, nil } -// GetJobs lists all current jobs from Slurm. -func (s *JobService) GetJobs(ctx context.Context) ([]model.JobResponse, error) { +// GetJobs lists all current jobs from Slurm with in-memory pagination. +func (s *JobService) GetJobs(ctx context.Context, query *model.JobListQuery) (*model.JobListResponse, error) { s.logger.Debug("slurm API request", zap.String("operation", "GetJobs"), ) @@ -109,11 +109,36 @@ func (s *JobService) GetJobs(ctx context.Context) ([]model.JobResponse, error) { zap.Any("body", result), ) - jobs := make([]model.JobResponse, 0, len(result.Jobs)) + allJobs := make([]model.JobResponse, 0, len(result.Jobs)) for i := range result.Jobs { - jobs = append(jobs, mapJobInfo(&result.Jobs[i])) + allJobs = append(allJobs, mapJobInfo(&result.Jobs[i])) } - return jobs, nil + + total := len(allJobs) + page := query.Page + pageSize := query.PageSize + if page < 1 { + page = 1 + } + if pageSize < 1 { + pageSize = 20 + } + + startIdx := (page - 1) * pageSize + end := startIdx + pageSize + if startIdx > total { + startIdx = total + } + if end > total { + end = total + } + + return &model.JobListResponse{ + Jobs: allJobs[startIdx:end], + Total: total, + Page: page, + PageSize: pageSize, + }, nil } // GetJob retrieves a single job by ID. If the job is not found in the active diff --git a/internal/service/job_service_test.go b/internal/service/job_service_test.go index 7efc295..49a2e26 100644 --- a/internal/service/job_service_test.go +++ b/internal/service/job_service_test.go @@ -149,14 +149,17 @@ func TestGetJobs(t *testing.T) { defer cleanup() svc := NewJobService(client, zap.NewNop()) - jobs, err := svc.GetJobs(context.Background()) + result, err := svc.GetJobs(context.Background(), &model.JobListQuery{Page: 1, PageSize: 20}) if err != nil { t.Fatalf("GetJobs: %v", err) } - if len(jobs) != 1 { - t.Fatalf("expected 1 job, got %d", len(jobs)) + if result.Total != 1 { + t.Fatalf("expected total 1, got %d", result.Total) } - j := jobs[0] + if len(result.Jobs) != 1 { + t.Fatalf("expected 1 job, got %d", len(result.Jobs)) + } + j := result.Jobs[0] if j.JobID != 100 { t.Errorf("expected JobID 100, got %d", j.JobID) } @@ -175,6 +178,12 @@ func TestGetJobs(t *testing.T) { if j.Nodes != "node01" { t.Errorf("expected Nodes node01, got %s", j.Nodes) } + if result.Page != 1 { + t.Errorf("expected Page 1, got %d", result.Page) + } + if result.PageSize != 20 { + t.Errorf("expected PageSize 20, got %d", result.PageSize) + } } func TestGetJob(t *testing.T) { @@ -621,7 +630,7 @@ func TestJobService_GetJobs_ErrorLog(t *testing.T) { defer srv.Close() svc, recorded := newJobServiceWithObserver(srv) - _, err := svc.GetJobs(context.Background()) + _, err := svc.GetJobs(context.Background(), &model.JobListQuery{Page: 1, PageSize: 20}) if err == nil { t.Fatal("expected error, got nil") }