Files
agentic-coding-workflow/internal/workspace/clone_runner.go
T
2026-06-10 15:30:52 +08:00

92 lines
3.1 KiB
Go

package workspace
import (
"context"
"log/slog"
"sync"
"time"
"github.com/google/uuid"
"github.com/yan1h/agent-coding-workflow/internal/audit"
"github.com/yan1h/agent-coding-workflow/internal/infra/git"
)
// CloneRunner 用 goroutine 异步跑 git clone。Schedule 是非阻塞的:
// 调用方立即返回,clone 进度通过 sync_status 列轮询。
//
// Plan #5(Job Runner)实现后会替换 Schedule 内部为 job dispatcher 入队,
// 调用点不需要改动。
type CloneRunner struct {
repo Repository
rec audit.Recorder
gitr git.Runner
log *slog.Logger
timeout time.Duration
wg sync.WaitGroup
resolveCred func(ctx context.Context, wsID uuid.UUID) (*git.Credential, error)
}
// NewCloneRunner 构造 CloneRunner。timeout==0 时使用默认 30 分钟超时。
func NewCloneRunner(
repo Repository, rec audit.Recorder, gitr git.Runner, log *slog.Logger,
resolveCred func(context.Context, uuid.UUID) (*git.Credential, error),
timeout time.Duration,
) *CloneRunner {
if timeout == 0 {
timeout = 30 * time.Minute
}
return &CloneRunner{
repo: repo, rec: rec, gitr: gitr, log: log,
timeout: timeout, resolveCred: resolveCred,
}
}
// Schedule 非阻塞地排程一次 clone;调用方立即返回。
func (r *CloneRunner) Schedule(wsID uuid.UUID) {
r.wg.Add(1)
go func() {
defer r.wg.Done()
ctx, cancel := context.WithTimeout(context.Background(), r.timeout)
defer cancel()
r.run(ctx, wsID)
}()
}
// Wait 阻塞直到所有已排程的 clone 任务完成;测试与 graceful shutdown 用。
func (r *CloneRunner) Wait() { r.wg.Wait() }
func (r *CloneRunner) run(ctx context.Context, wsID uuid.UUID) {
ws, err := r.repo.GetWorkspaceByID(ctx, wsID)
if err != nil {
r.log.Error("clone runner: get workspace", "ws_id", wsID, "err", err.Error())
return
}
// 状态/审计写操作与 clone 的超时 ctx 解耦:clone 超时后 ctx 已 DeadlineExceeded,
// 仍需把状态写成 error,否则永久卡在 cloning。
writeCtx := context.WithoutCancel(ctx)
cred, err := r.resolveCred(ctx, wsID)
if err != nil {
r.markError(writeCtx, wsID, "resolve credential: "+err.Error())
return
}
if cloneErr := r.gitr.Clone(ctx, ws.MainPath, ws.GitRemoteURL, ws.DefaultBranch, cred); cloneErr != nil {
r.markError(writeCtx, wsID, summarizeErr(cloneErr))
_ = r.rec.Record(writeCtx, audit.Entry{Action: "workspace.clone_failed", TargetType: "workspace", TargetID: wsID.String(), Metadata: map[string]any{"err": summarizeErr(cloneErr)}})
return
}
now := time.Now().UTC()
if _, err := r.repo.UpdateWorkspaceSyncStatus(writeCtx, wsID, SyncStatusIdle, &now, ""); err != nil {
r.log.Error("clone runner: update sync status idle", "ws_id", wsID, "err", err.Error())
return
}
_ = r.rec.Record(writeCtx, audit.Entry{Action: "workspace.clone_ok", TargetType: "workspace", TargetID: wsID.String()})
}
func (r *CloneRunner) markError(ctx context.Context, wsID uuid.UUID, msg string) {
if _, err := r.repo.UpdateWorkspaceSyncStatus(ctx, wsID, SyncStatusError, nil, msg); err != nil {
r.log.Error("clone runner: update sync status error", "ws_id", wsID, "err", err.Error())
}
}