You've already forked agentic-coding-workflow
92 lines
3.1 KiB
Go
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())
|
|
}
|
|
}
|