From f6e69200944a1fbc0f1fc0ecd8beb2f8659161c1 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 29 May 2026 16:46:50 +0300 Subject: refactor(sync): move backup session state to Syncer instance (qq) --- internal/sync/backup_test.go | 63 +++++++++++++++++++++++++++++++++++++++----- internal/sync/sync.go | 15 +++-------- 2 files changed, 60 insertions(+), 18 deletions(-) diff --git a/internal/sync/backup_test.go b/internal/sync/backup_test.go index 9bdbff3..fd15d04 100644 --- a/internal/sync/backup_test.go +++ b/internal/sync/backup_test.go @@ -2,15 +2,15 @@ package sync import ( "errors" + "fmt" + stdsync "sync" + "sync/atomic" "testing" "codeberg.org/snonux/gitsyncer/internal/config" ) func TestHandlePushError_DisablesBackupForSession(t *testing.T) { - resetBackupSessionState() - t.Cleanup(resetBackupSessionState) - syncer := &Syncer{} syncer.SetBackupEnabled(true) @@ -24,9 +24,6 @@ func TestHandlePushError_DisablesBackupForSession(t *testing.T) { } func TestHandlePushError_PropagatesPrimaryRemoteFailure(t *testing.T) { - resetBackupSessionState() - t.Cleanup(resetBackupSessionState) - syncer := &Syncer{} syncer.SetBackupEnabled(true) @@ -37,6 +34,60 @@ func TestHandlePushError_PropagatesPrimaryRemoteFailure(t *testing.T) { } } +func TestHandlePushError_BackupDisableIsIsolatedPerSyncer(t *testing.T) { + backupOrg := &config.Organization{BackupLocation: true} + + syncerA := &Syncer{} + syncerA.SetBackupEnabled(true) + + syncerB := &Syncer{} + syncerB.SetBackupEnabled(true) + + err := syncerA.handlePushError("backup-a", backupOrg, errors.New("dial tcp: connection refused")) + if err != nil { + t.Fatalf("expected backup push failure to be downgraded, got %v", err) + } + + if syncerA.backupActive() { + t.Fatal("expected syncerA backup sync to be disabled for the remainder of the session") + } + if !syncerB.backupActive() { + t.Fatal("expected syncerB backup session to remain active") + } +} + +func TestBackupSessionState_DisableIsThreadSafe(t *testing.T) { + var session backupSessionState + var firstDisableCount atomic.Int32 + + const workers = 32 + var wg stdsync.WaitGroup + wg.Add(workers) + + for i := 0; i < workers; i++ { + go func(i int) { + defer wg.Done() + if session.disable(fmt.Sprintf("reason-%d", i)) { + firstDisableCount.Add(1) + } + }(i) + } + + wg.Wait() + + if got := firstDisableCount.Load(); got != 1 { + t.Fatalf("expected exactly one successful disable transition, got %d", got) + } + + disabled, reason := session.status() + if !disabled { + t.Fatal("expected backup session to be disabled") + } + if reason == "" { + t.Fatal("expected disable reason to be recorded") + } +} + func TestParseSSHLocation_SupportsSSHURLWithPort(t *testing.T) { t.Parallel() diff --git a/internal/sync/sync.go b/internal/sync/sync.go index e0a8215..2f05110 100644 --- a/internal/sync/sync.go +++ b/internal/sync/sync.go @@ -17,8 +17,6 @@ type backupSessionState struct { reason string } -var currentBackupSession backupSessionState - // Syncer handles repository synchronization between organizations type Syncer struct { config *config.Config @@ -27,6 +25,7 @@ type Syncer struct { abandonedReports map[string]*AbandonedBranchReport // Collects reports across repos branchFilter *BranchFilter // Filter for excluding branches backupEnabled bool // Whether to sync to backup locations + backupSession backupSessionState } // CLAUDE: Is there a reason, we return a pointer to Syncer? @@ -59,7 +58,7 @@ func (s *Syncer) backupActive() bool { return false } - disabled, _ := currentBackupSession.status() + disabled, _ := s.backupSession.status() return !disabled } @@ -69,7 +68,7 @@ func (s *Syncer) disableBackupForSession(remoteName string, err error) { } reason := fmt.Sprintf("%s: %v", remoteName, err) - if currentBackupSession.disable(reason) { + if s.backupSession.disable(reason) { fmt.Printf("Warning: Backup sync to %s failed: %v\n", remoteName, err) fmt.Println("Warning: Disabling backup sync for the remainder of this session.") } @@ -95,14 +94,6 @@ func (b *backupSessionState) status() (bool, string) { return b.disabled, b.reason } -func resetBackupSessionState() { - currentBackupSession.mu.Lock() - defer currentBackupSession.mu.Unlock() - - currentBackupSession.disabled = false - currentBackupSession.reason = "" -} - // SyncRepository synchronizes a repository across all configured organizations func (s *Syncer) SyncRepository(repoName string) error { s.repoName = repoName -- cgit v1.2.3