summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-29 16:46:50 +0300
committerPaul Buetow <paul@buetow.org>2026-05-29 16:46:50 +0300
commitf6e69200944a1fbc0f1fc0ecd8beb2f8659161c1 (patch)
treee4b52e6449a476b683ada33397d1c8ecfded79f7
parent1125cdcda9590c81308ca82e6b0eb9fcb10e7a6e (diff)
refactor(sync): move backup session state to Syncer instance (qq)
-rw-r--r--internal/sync/backup_test.go63
-rw-r--r--internal/sync/sync.go15
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