feat(render+admin): stop a render job (admin, any owner)
Build backend images / build content-svc (push) Failing after 1m0s
Build backend images / build file-svc (push) Failing after 1m3s
Build backend images / build gateway (push) Failing after 1m2s
Build backend images / build identity-svc (push) Failing after 1m20s
Build backend images / build notification-svc (push) Failing after 1m13s
Build backend images / build render-svc (push) Failing after 1m5s
Build backend images / build studio-svc (push) Failing after 1m0s
Build backend images / build content-svc (push) Failing after 1m0s
Build backend images / build file-svc (push) Failing after 1m3s
Build backend images / build gateway (push) Failing after 1m2s
Build backend images / build identity-svc (push) Failing after 1m20s
Build backend images / build notification-svc (push) Failing after 1m13s
Build backend images / build render-svc (push) Failing after 1m5s
Build backend images / build studio-svc (push) Failing after 1m0s
The render-queue cancel button used the owner-scoped /cancel (WHERE user_id=…),
so an admin couldn't stop another user's job. Added:
- render-svc: POST /v1/renders/:job_id/stop (admin-gated) → store.StopJob cancels
any in-progress job regardless of owner and frees the assigned node
- admin: render-queue button now "توقف" → /api/admin/renders/{id}/stop (with confirm)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -90,6 +90,7 @@ func main() {
|
||||
renders.POST("", renderH.Create)
|
||||
renders.GET("/:job_id", renderH.Get)
|
||||
renders.POST("/:job_id/cancel", renderH.Cancel)
|
||||
renders.POST("/:job_id/stop", admin, renderH.Stop)
|
||||
renders.POST("/:job_id/retry", renderH.Retry)
|
||||
renders.GET("/:job_id/progress", renderH.Progress)
|
||||
renders.GET("/:job_id/logs", renderH.Logs)
|
||||
|
||||
@@ -597,6 +597,24 @@ func (s *Store) CancelJob(ctx context.Context, id, userID uuid.UUID) (bool, erro
|
||||
return tag.RowsAffected() > 0, err
|
||||
}
|
||||
|
||||
// StopJob cancels any in-progress job regardless of owner (admin action). Also
|
||||
// frees the assigned node so it can pick up new work.
|
||||
func (s *Store) StopJob(ctx context.Context, id uuid.UUID) (bool, error) {
|
||||
tag, err := s.pool.Exec(ctx, `
|
||||
UPDATE render.render_jobs
|
||||
SET step = 'Cancelled'::render_step, completed_at = NOW(), updated_at = NOW()
|
||||
WHERE id = $1 AND step NOT IN ('Done','Failed','Cancelled')`,
|
||||
id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// Release any node that was actively working this job.
|
||||
_, _ = s.pool.Exec(ctx,
|
||||
`UPDATE render.render_nodes SET status = 'Ready'::node_status, current_frame_job_id = NULL, updated_at = NOW()
|
||||
WHERE current_frame_job_id IN (SELECT id FROM render.frame_jobs WHERE render_job_id = $1)`, id)
|
||||
return tag.RowsAffected() > 0, err
|
||||
}
|
||||
|
||||
func (s *Store) GetJobProgress(ctx context.Context, id, userID uuid.UUID) (*models.RenderJob, error) {
|
||||
return s.GetJobByID(ctx, id, userID)
|
||||
}
|
||||
|
||||
@@ -131,6 +131,21 @@ func (h *RenderHandler) Cancel(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// POST /v1/renders/:job_id/stop — admin: stop any user's in-progress job
|
||||
func (h *RenderHandler) Stop(c *gin.Context) {
|
||||
jobID, err := uuid.Parse(c.Param("job_id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid job_id"})
|
||||
return
|
||||
}
|
||||
stopped, err := h.store.StopJob(c.Request.Context(), jobID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"stopped": stopped})
|
||||
}
|
||||
|
||||
// POST /v1/renders/:job_id/retry
|
||||
func (h *RenderHandler) Retry(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
|
||||
Reference in New Issue
Block a user