Files
xinmi/internal/workbench/job_control.go
T
yawan f7fc992f57
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
2026-09-22 16:19:06 +08:00

105 lines
3.4 KiB
Go

package workbench
import (
"context"
"fmt"
"time"
"github.com/Tencent/WeKnora/internal/accesspolicy"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// Lock the resource before its job, matching the generation commit order.
// Revoking a lease prevents a late model response from replacing newer content.
func (s *Service) controlJob(ctx context.Context, id, action string) (*Job, error) {
tenant, user, _, err := identity(ctx)
if err != nil {
return nil, err
}
if action != "pause" && action != "cancel" && action != "resume" && action != "retry" {
return nil, ErrInvalid
}
var job Job
if err = s.db.WithContext(ctx).Where("id=? AND tenant_id=? AND owner_id=?", id, tenant, user).Take(&job).Error; err != nil {
return nil, err
}
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
resource, err := lockResource(ctx, tx, job.Kind, job.ResourceID)
if err != nil {
return err
}
if err = tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id=? AND tenant_id=? AND owner_id=?", id, tenant, user).Take(&job).Error; err != nil {
return err
}
if !accesspolicy.Allowed(ctx, accesspolicy.WorkbenchActionPermission(job.Kind, job.Action)) {
return ErrForbidden
}
request, err := decode[ActionRequest](job.Request)
if err != nil {
return ErrInvalid
}
if resource.Version != request.Version {
return fmt.Errorf("%w: 内容已有新版本,请从当前内容重新发起操作", ErrConflict)
}
if action == "retry" || action == "resume" {
if (action == "resume" && job.Status != "paused") || (action == "retry" && job.Status != "failed" && job.Status != "cancelled") {
return ErrConflict
}
if err = requireExecutable(resource); err != nil {
return err
}
if err = requireIdle(tx, tenant, resource.ID); err != nil {
return err
}
job = Job{ID: uuid.NewString(), TenantID: tenant, OwnerID: user, ResourceID: resource.ID, Kind: resource.Kind, Action: job.Action, Status: "queued", Stage: "等待执行", Request: job.Request, CreatedAt: time.Now(), UpdatedAt: time.Now()}
return tx.Create(&job).Error
}
if job.Status != "queued" && job.Status != "running" && !(action == "cancel" && job.Status == "paused") {
return ErrConflict
}
status, stage := "cancelled", "已取消"
if action == "pause" {
status, stage = "paused", "已暂停,继续时重新执行本步"
}
if err = tx.Model(&job).Updates(map[string]any{"status": status, "stage": stage, "lease_token": "", "lease_until": nil, "updated_at": time.Now()}).Error; err != nil {
return err
}
return finishConsumption(tx, &job, false)
})
if err != nil {
return nil, err
}
_ = s.event(ctx, job.ResourceID, action, actionName(action), "info")
return &job, nil
}
func (h *Handler) ControlJob(c *gin.Context) {
job, err := h.service.controlJob(c.Request.Context(), c.Param("id"), c.Param("action"))
if err != nil {
respondError(c, err)
return
}
result(c, job)
}
func (s *Service) watchJobLease(ctx context.Context, cancel context.CancelFunc, job *Job) {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
var count int64
err := s.db.WithContext(ctx).Model(&Job{}).Where("id=? AND lease_token=? AND status=? AND lease_until>?", job.ID, job.LeaseToken, "running", time.Now()).Count(&count).Error
if err == nil && count == 0 {
cancel()
return
}
}
}
}