Add generic AI job queue lifecycle

This commit is contained in:
Grendgi
2026-06-08 13:32:43 +03:00
parent e2f2adf900
commit 9d65ee323c
9 changed files with 262 additions and 5 deletions

View File

@@ -39,10 +39,16 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.handleCreateJob(w, r)
case r.Method == http.MethodPost && path == "/api/v1/jobs/batch":
s.handleCreateBatch(w, r)
case r.Method == http.MethodPost && path == "/api/v1/jobs/claim":
s.handleClaimJobs(w, r)
case r.Method == http.MethodGet && strings.HasPrefix(path, "/api/v1/jobs/"):
s.handleGetJob(w, r, path)
case r.Method == http.MethodPost && strings.HasPrefix(path, "/api/v1/jobs/") && strings.HasSuffix(path, "/retry"):
s.handleRetryJob(w, r, path)
case r.Method == http.MethodPost && strings.HasPrefix(path, "/api/v1/jobs/") && strings.HasSuffix(path, "/complete"):
s.handleCompleteJob(w, r, path)
case r.Method == http.MethodPost && strings.HasPrefix(path, "/api/v1/jobs/") && strings.HasSuffix(path, "/fail"):
s.handleFailJob(w, r, path)
case r.Method == http.MethodGet && path == "/api/v1/stats":
s.handleStats(w, r)
default:
@@ -136,6 +142,26 @@ func (s *Server) handleCreateBatch(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusCreated, out)
}
type claimJobsResponse struct {
Jobs []*model.Job `json:"jobs"`
}
func (s *Server) handleClaimJobs(w http.ResponseWriter, r *http.Request) {
var req model.ClaimJobs
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "bad json")
return
}
ctx, cancel := contextWithTimeout(r, 8*time.Second)
defer cancel()
jobs, err := s.store.ClaimJobs(ctx, req)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, claimJobsResponse{Jobs: jobs})
}
func (s *Server) handleGetJob(w http.ResponseWriter, r *http.Request, path string) {
id, err := jobIDFromPath(path, false)
if err != nil {
@@ -176,6 +202,56 @@ func (s *Server) handleRetryJob(w http.ResponseWriter, r *http.Request, path str
writeJSON(w, http.StatusOK, job)
}
func (s *Server) handleCompleteJob(w http.ResponseWriter, r *http.Request, path string) {
id, err := jobIDFromActionPath(path, "complete")
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
var req model.CompleteJob
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "bad json")
return
}
ctx, cancel := contextWithTimeout(r, 8*time.Second)
defer cancel()
job, err := s.store.CompleteJob(ctx, id, req)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if job == nil {
writeError(w, http.StatusNotFound, "running job not found")
return
}
writeJSON(w, http.StatusOK, job)
}
func (s *Server) handleFailJob(w http.ResponseWriter, r *http.Request, path string) {
id, err := jobIDFromActionPath(path, "fail")
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
var req model.FailJob
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "bad json")
return
}
ctx, cancel := contextWithTimeout(r, 8*time.Second)
defer cancel()
job, err := s.store.FailJob(ctx, id, req)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if job == nil {
writeError(w, http.StatusNotFound, "running job not found")
return
}
writeJSON(w, http.StatusOK, job)
}
func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
ctx, cancel := contextWithTimeout(r, 8*time.Second)
defer cancel()
@@ -199,6 +275,16 @@ func jobIDFromPath(path string, retry bool) (uuid.UUID, error) {
return id, nil
}
func jobIDFromActionPath(path string, action string) (uuid.UUID, error) {
raw := strings.TrimPrefix(path, "/api/v1/jobs/")
raw = strings.TrimSuffix(raw, "/"+action)
id, err := uuid.Parse(strings.Trim(raw, "/"))
if err != nil {
return uuid.Nil, errors.New("bad job id")
}
return id, nil
}
func isValidationError(err error) bool {
msg := err.Error()
return strings.Contains(msg, " is required")