♻️ Refactor synchronization service
This commit is contained in:
283
internal/syncer/app.go
Normal file
283
internal/syncer/app.go
Normal file
@@ -0,0 +1,283 @@
|
||||
package syncer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
"voidraft/internal/models/ent"
|
||||
"voidraft/internal/syncer/backend"
|
||||
gitbackend "voidraft/internal/syncer/backend/git"
|
||||
snapshotstorebackend "voidraft/internal/syncer/backend/snapshotstore"
|
||||
localfsblob "voidraft/internal/syncer/backend/snapshotstore/blob/localfs"
|
||||
"voidraft/internal/syncer/engine"
|
||||
"voidraft/internal/syncer/merge"
|
||||
"voidraft/internal/syncer/resource"
|
||||
"voidraft/internal/syncer/scheduler"
|
||||
"voidraft/internal/syncer/snapshot"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultAuthorName = "voidraft"
|
||||
defaultAuthorEmail = "sync@voidraft.app"
|
||||
defaultSyncAttempts = 3
|
||||
)
|
||||
|
||||
// Options 描述同步应用的构造选项。
|
||||
type Options struct {
|
||||
Logger Logger
|
||||
MaxSyncAttempts int
|
||||
}
|
||||
|
||||
// App 是同步系统的编排入口。
|
||||
type App struct {
|
||||
logger Logger
|
||||
snapshotter snapshot.Snapshotter
|
||||
store snapshot.Store
|
||||
merger merge.Merger
|
||||
maxSyncAttempts int
|
||||
|
||||
mu sync.RWMutex
|
||||
syncMu sync.Mutex
|
||||
config Config
|
||||
schedulers map[string]*scheduler.Ticker
|
||||
}
|
||||
|
||||
// NewApp 创建新的同步应用实例。
|
||||
func NewApp(client *ent.Client, options Options) *App {
|
||||
maxSyncAttempts := options.MaxSyncAttempts
|
||||
if maxSyncAttempts <= 0 {
|
||||
maxSyncAttempts = defaultSyncAttempts
|
||||
}
|
||||
|
||||
return &App{
|
||||
logger: options.Logger,
|
||||
snapshotter: resource.NewRegistry(
|
||||
resource.NewDocumentAdapter(client),
|
||||
resource.NewExtensionAdapter(client),
|
||||
resource.NewKeyBindingAdapter(client),
|
||||
resource.NewThemeAdapter(client),
|
||||
),
|
||||
store: snapshot.NewFileStore(),
|
||||
merger: merge.NewUpdatedAtWinsMerger(),
|
||||
maxSyncAttempts: maxSyncAttempts,
|
||||
schedulers: make(map[string]*scheduler.Ticker),
|
||||
}
|
||||
}
|
||||
|
||||
// Reconfigure 更新同步系统配置。
|
||||
func (a *App) Reconfigure(ctx context.Context, cfg Config) error {
|
||||
_ = ctx
|
||||
|
||||
normalized := cfg.Normalize()
|
||||
for _, target := range normalized.Targets {
|
||||
if err := target.Validate(); err != nil {
|
||||
return fmt.Errorf("validate target %s: %w", target.Kind, err)
|
||||
}
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
a.config = normalized
|
||||
a.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start 按当前配置启动自动同步调度。
|
||||
func (a *App) Start(ctx context.Context) error {
|
||||
targets := a.targetsSnapshot()
|
||||
if err := a.verifyTargets(ctx, targets); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
a.stopSchedulersLocked()
|
||||
|
||||
for _, target := range targets {
|
||||
if !target.Ready() || !target.Schedule.AutoSync || target.Schedule.Interval <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
currentTargetID := target.Kind
|
||||
task := scheduler.NewTicker()
|
||||
task.Start(target.Schedule.Interval, func(runCtx context.Context) error {
|
||||
_, err := a.Sync(runCtx, currentTargetID)
|
||||
if err != nil && a.logger != nil {
|
||||
a.logger.Error("sync auto run failed for target %s: %v", currentTargetID, err)
|
||||
}
|
||||
return err
|
||||
})
|
||||
a.schedulers[currentTargetID] = task
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop 停止所有自动同步调度。
|
||||
func (a *App) Stop(ctx context.Context) error {
|
||||
_ = ctx
|
||||
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
a.stopSchedulersLocked()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sync 执行指定目标的一次完整同步。
|
||||
func (a *App) Sync(ctx context.Context, targetID string) (*SyncResult, error) {
|
||||
target, err := a.currentTarget(targetID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !target.Enabled {
|
||||
return nil, ErrTargetDisabled
|
||||
}
|
||||
if !target.Ready() {
|
||||
return nil, ErrTargetNotReady
|
||||
}
|
||||
|
||||
backendInstance, err := a.newBackend(target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = backendInstance.Close()
|
||||
}()
|
||||
|
||||
syncEngine := engine.NewSyncEngine(
|
||||
backendInstance,
|
||||
a.store,
|
||||
a.snapshotter,
|
||||
a.merger,
|
||||
engine.Options{
|
||||
Logger: a.logger,
|
||||
MaxAttempts: a.maxSyncAttempts,
|
||||
},
|
||||
)
|
||||
|
||||
a.syncMu.Lock()
|
||||
defer a.syncMu.Unlock()
|
||||
|
||||
result, err := syncEngine.Sync(ctx, engine.SyncOptions{
|
||||
CommitMessage: a.commitMessage(target),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SyncResult{
|
||||
TargetID: target.Kind,
|
||||
LocalChanged: result.LocalChanged,
|
||||
RemoteChanged: result.RemoteChanged,
|
||||
AppliedToLocal: result.AppliedToLocal,
|
||||
Published: result.Published,
|
||||
ConflictCount: result.ConflictCount,
|
||||
Revision: result.Revision,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// commitMessage 生成提交信息。
|
||||
func (a *App) commitMessage(target TargetConfig) string {
|
||||
return fmt.Sprintf("Sync %s %s", target.Kind, time.Now().Format(time.RFC3339))
|
||||
}
|
||||
|
||||
// currentTarget 返回当前内存中的目标配置。
|
||||
func (a *App) currentTarget(targetID string) (TargetConfig, error) {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
return a.config.Target(targetID)
|
||||
}
|
||||
|
||||
// targetsSnapshot 返回当前所有目标的快照。
|
||||
func (a *App) targetsSnapshot() []TargetConfig {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
|
||||
targets := make([]TargetConfig, len(a.config.Targets))
|
||||
copy(targets, a.config.Targets)
|
||||
return targets
|
||||
}
|
||||
|
||||
// verifyTargets 预先校验所有已就绪目标。
|
||||
func (a *App) verifyTargets(ctx context.Context, targets []TargetConfig) error {
|
||||
for _, target := range targets {
|
||||
if !target.Ready() {
|
||||
continue
|
||||
}
|
||||
|
||||
backendInstance, err := a.newBackend(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
verifyErr := backendInstance.Verify(ctx)
|
||||
closeErr := backendInstance.Close()
|
||||
if verifyErr != nil {
|
||||
return fmt.Errorf("verify target %s: %w", target.Kind, verifyErr)
|
||||
}
|
||||
if closeErr != nil {
|
||||
return fmt.Errorf("close target %s backend: %w", target.Kind, closeErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// newBackend 根据目标配置构造后端实例。
|
||||
func (a *App) newBackend(target TargetConfig) (backend.Backend, error) {
|
||||
switch target.Kind {
|
||||
case TargetKindGit:
|
||||
if target.Git == nil {
|
||||
return nil, fmt.Errorf("target %s: git config is nil", target.Kind)
|
||||
}
|
||||
return gitbackend.New(gitbackend.Config{
|
||||
RepoPath: target.Git.RepoPath,
|
||||
RepoURL: target.Git.RepoURL,
|
||||
Branch: target.Git.Branch,
|
||||
RemoteName: target.Git.RemoteName,
|
||||
AuthorName: fallbackString(target.Git.AuthorName, defaultAuthorName),
|
||||
AuthorEmail: fallbackString(target.Git.AuthorEmail, defaultAuthorEmail),
|
||||
Auth: gitbackend.AuthConfig{
|
||||
Method: target.Git.Auth.Method,
|
||||
Username: target.Git.Auth.Username,
|
||||
Password: target.Git.Auth.Password,
|
||||
Token: target.Git.Auth.Token,
|
||||
SSHKeyPath: target.Git.Auth.SSHKeyPath,
|
||||
SSHKeyPassword: target.Git.Auth.SSHKeyPassword,
|
||||
},
|
||||
})
|
||||
case TargetKindLocalFS:
|
||||
if target.LocalFS == nil {
|
||||
return nil, fmt.Errorf("target %s: localfs config is nil", target.Kind)
|
||||
}
|
||||
store, err := localfsblob.New(target.LocalFS.RootPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return snapshotstorebackend.New(snapshotstorebackend.Config{
|
||||
Store: store,
|
||||
Namespace: target.LocalFS.Namespace,
|
||||
HeadKey: target.LocalFS.HeadKey,
|
||||
})
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnsupportedBackend, target.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
// stopSchedulersLocked 停止所有调度器。
|
||||
func (a *App) stopSchedulersLocked() {
|
||||
for targetID, task := range a.schedulers {
|
||||
task.Stop()
|
||||
delete(a.schedulers, targetID)
|
||||
}
|
||||
}
|
||||
|
||||
// fallbackString 返回第一个非空字符串。
|
||||
func fallbackString(value string, fallback string) string {
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
Reference in New Issue
Block a user