♻️ Refactor synchronization service
This commit is contained in:
184
internal/syncer/engine/sync_engine.go
Normal file
184
internal/syncer/engine/sync_engine.go
Normal file
@@ -0,0 +1,184 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user