package engine import ( "context" "errors" "fmt" "os" "voidraft/internal/syncer/backend" "voidraft/internal/syncer/merge" "voidraft/internal/syncer/snapshot" ) const defaultMaxAttempts = 3 // Logger 描述同步引擎依赖的最小日志接口。 type Logger interface { Debug(message string, args ...interface{}) Info(message string, args ...interface{}) Warning(message string, args ...interface{}) Error(message string, args ...interface{}) } // Options 描述同步引擎构造选项。 type Options struct { Logger Logger MaxAttempts int } // SyncOptions 描述一次同步执行参数。 type SyncOptions struct { CommitMessage string } // Result 描述同步引擎执行结果。 type Result struct { LocalChanged bool RemoteChanged bool AppliedToLocal bool Published bool ConflictCount int Revision string } // SyncEngine 负责执行一次完整的同步闭环。 type SyncEngine struct { backend backend.Backend store snapshot.Store snapshotter snapshot.Snapshotter merger merge.Merger logger Logger maxAttempts int } // NewSyncEngine 创建新的同步引擎实例。 func NewSyncEngine( backendInstance backend.Backend, store snapshot.Store, snapshotter snapshot.Snapshotter, merger merge.Merger, options Options, ) *SyncEngine { maxAttempts := options.MaxAttempts if maxAttempts <= 0 { maxAttempts = defaultMaxAttempts } return &SyncEngine{ backend: backendInstance, store: store, snapshotter: snapshotter, merger: merger, logger: options.Logger, maxAttempts: maxAttempts, } } // Sync 执行同步,并在远端版本竞争时自动重试。 func (e *SyncEngine) Sync(ctx context.Context, options SyncOptions) (*Result, error) { var lastErr error for attempt := 1; attempt <= e.maxAttempts; attempt++ { result, retry, err := e.syncOnce(ctx, options) if err == nil { return result, nil } if retry && errors.Is(err, backend.ErrRevisionConflict) { lastErr = err if e.logger != nil { e.logger.Warning("sync retry after revision conflict, attempt %d/%d", attempt, e.maxAttempts) } continue } return nil, err } if lastErr == nil { lastErr = backend.ErrRevisionConflict } return nil, lastErr } // syncOnce 执行一次同步尝试。 func (e *SyncEngine) syncOnce(ctx context.Context, options SyncOptions) (*Result, bool, error) { localSnapshot, err := e.snapshotter.Export(ctx) if err != nil { return nil, false, fmt.Errorf("export local snapshot: %w", err) } localDigest, err := snapshot.Digest(localSnapshot) if err != nil { return nil, false, fmt.Errorf("digest local snapshot: %w", err) } remoteDir, err := os.MkdirTemp("", "voidraft-sync-remote-*") if err != nil { return nil, false, err } defer os.RemoveAll(remoteDir) remoteState, err := e.backend.DownloadLatest(ctx, remoteDir) if err != nil { return nil, false, fmt.Errorf("download remote snapshot: %w", err) } remoteSnapshot := snapshot.New() if remoteState.Exists { remoteSnapshot, err = e.store.Read(ctx, remoteDir) if err != nil { return nil, false, fmt.Errorf("read remote snapshot: %w", err) } } remoteDigest, err := snapshot.Digest(remoteSnapshot) if err != nil { return nil, false, fmt.Errorf("digest remote snapshot: %w", err) } mergedSnapshot, report, err := e.merger.Merge(ctx, localSnapshot, remoteSnapshot) if err != nil { return nil, false, fmt.Errorf("merge snapshot: %w", err) } mergedDigest, err := snapshot.Digest(mergedSnapshot) if err != nil { return nil, false, fmt.Errorf("digest merged snapshot: %w", err) } appliedToLocal := localDigest != mergedDigest if appliedToLocal { if err := e.snapshotter.Apply(ctx, mergedSnapshot); err != nil { return nil, false, fmt.Errorf("apply merged snapshot: %w", err) } } stageDir, err := os.MkdirTemp("", "voidraft-sync-stage-*") if err != nil { return nil, false, err } defer os.RemoveAll(stageDir) if err := e.store.Write(ctx, stageDir, mergedSnapshot); err != nil { return nil, false, fmt.Errorf("write merged snapshot: %w", err) } publishedState, err := e.backend.Upload(ctx, stageDir, backend.PublishOptions{ ExpectedRevision: remoteState.Revision, Message: options.CommitMessage, }) if err != nil { if errors.Is(err, backend.ErrRevisionConflict) { return nil, true, err } return nil, false, fmt.Errorf("upload merged snapshot: %w", err) } return &Result{ LocalChanged: appliedToLocal, RemoteChanged: remoteDigest != mergedDigest, AppliedToLocal: appliedToLocal, Published: remoteState != publishedState, ConflictCount: report.Conflicts, Revision: publishedState.Revision, }, false, nil }