185 lines
4.6 KiB
Go
185 lines
4.6 KiB
Go
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
|
|
}
|