Skip to content

Commit adc2a21

Browse files
rremerwxiaoguang
authored andcommitted
Fail mirroring more gracefully (go-gitea#34002)
* reuse recoverable error checks across mirror_pull * add new cases for 'cannot lock ref/not our ref' (race condition in fetch) and 'Unable to create/lock" * move lfs sync right after commit graph write, and before other maintenance which may fail * try a prune for 'broken reference' as well as 'not our ref' * always sync LFS right after commit graph write, and before other maintenance which may fail This handles a few cases where our very large and very active repositories could serve mirrored git refs, but be missing lfs files: ## Case 1 (multiple variants): Race condition in git fetch There was already a check for 'unable to resolve reference' on a failed git fetch, after which a git prune and then subsequent fetch are performed. This is to work around a race condition where the git remote tells Gitea about a ref for some HEAD of a branch, then fails a few seconds later because the remote branch was deleted, or the ref was updated (force push). There are two more variants to the error message you can get, but for the same kind of race condition. These *may* be related to the git binary version Gitea has access to (in my case, it was 2.48.1). ## Case 2: githttp.go can serve updated git refs before it's synced lfs oids There is probably a more aggressive refactor we could do here to have the cat-file loop use FETCH_HEAD instead of relying on the commit graphs to be committed locally (and thus serveable to clients of Gitea), but a simple reduction in the occurrences of this for me was to move the lfs sync block immediately after the commit-graph write and before any other time-consuming (or potentially erroring/exiting) blocks. --------- Co-authored-by: wxiaoguang <[email protected]> (cherry picked from commit e0ad72e)
1 parent 6d5fc19 commit adc2a21

File tree

2 files changed

+57
-11
lines changed

2 files changed

+57
-11
lines changed

services/mirror/mirror_pull.go

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,24 @@ func pruneBrokenReferences(ctx context.Context,
244244
return pruneErr
245245
}
246246

247+
// checkRecoverableSyncError takes an error message from a git fetch command and returns false if it should be a fatal/blocking error
248+
func checkRecoverableSyncError(stderrMessage string) bool {
249+
switch {
250+
case strings.Contains(stderrMessage, "unable to resolve reference") && strings.Contains(stderrMessage, "reference broken"):
251+
return true
252+
case strings.Contains(stderrMessage, "remote error") && strings.Contains(stderrMessage, "not our ref"):
253+
return true
254+
case strings.Contains(stderrMessage, "cannot lock ref") && strings.Contains(stderrMessage, "but expected"):
255+
return true
256+
case strings.Contains(stderrMessage, "cannot lock ref") && strings.Contains(stderrMessage, "unable to resolve reference"):
257+
return true
258+
case strings.Contains(stderrMessage, "Unable to create") && strings.Contains(stderrMessage, ".lock"):
259+
return true
260+
default:
261+
return false
262+
}
263+
}
264+
247265
// runSync returns true if sync finished without error.
248266
func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bool) {
249267
repoPath := m.Repo.RepoPath()
@@ -286,7 +304,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
286304
stdoutMessage := util.SanitizeCredentialURLs(stdout)
287305

288306
// Now check if the error is a resolve reference due to broken reference
289-
if strings.Contains(stderr, "unable to resolve reference") && strings.Contains(stderr, "reference broken") {
307+
if checkRecoverableSyncError(stderr) {
290308
log.Warn("SyncMirrors [repo: %-v]: failed to update mirror repository due to broken references:\nStdout: %s\nStderr: %s\nErr: %v\nAttempting Prune", m.Repo, stdoutMessage, stderrMessage, err)
291309
err = nil
292310

@@ -337,6 +355,15 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
337355
return nil, false
338356
}
339357

358+
if m.LFS && setting.LFS.StartServer {
359+
log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo)
360+
endpoint := lfs.DetermineEndpoint(remoteURL.String(), m.LFSEndpoint)
361+
lfsClient := lfs.NewClient(endpoint, nil)
362+
if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, m.Repo, gitRepo, lfsClient); err != nil {
363+
log.Error("SyncMirrors [repo: %-v]: failed to synchronize LFS objects for repository: %v", m.Repo, err)
364+
}
365+
}
366+
340367
log.Trace("SyncMirrors [repo: %-v]: syncing branches...", m.Repo)
341368
if _, err = repo_module.SyncRepoBranchesWithRepo(ctx, m.Repo, gitRepo, 0); err != nil {
342369
log.Error("SyncMirrors [repo: %-v]: failed to synchronize branches: %v", m.Repo, err)
@@ -346,15 +373,6 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
346373
if err = repo_module.SyncReleasesWithTags(ctx, m.Repo, gitRepo); err != nil {
347374
log.Error("SyncMirrors [repo: %-v]: failed to synchronize tags to releases: %v", m.Repo, err)
348375
}
349-
350-
if m.LFS && setting.LFS.StartServer {
351-
log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo)
352-
endpoint := lfs.DetermineEndpoint(remoteURL.String(), m.LFSEndpoint)
353-
lfsClient := lfs.NewClient(endpoint, nil)
354-
if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, m.Repo, gitRepo, lfsClient); err != nil {
355-
log.Error("SyncMirrors [repo: %-v]: failed to synchronize LFS objects for repository: %v", m.Repo, err)
356-
}
357-
}
358376
gitRepo.Close()
359377

360378
log.Trace("SyncMirrors [repo: %-v]: updating size of repository", m.Repo)
@@ -382,7 +400,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
382400
stdoutMessage := util.SanitizeCredentialURLs(stdout)
383401

384402
// Now check if the error is a resolve reference due to broken reference
385-
if strings.Contains(stderrMessage, "unable to resolve reference") && strings.Contains(stderrMessage, "reference broken") {
403+
if checkRecoverableSyncError(stderrMessage) {
386404
log.Warn("SyncMirrors [repo: %-v Wiki]: failed to update mirror wiki repository due to broken references:\nStdout: %s\nStderr: %s\nErr: %v\nAttempting Prune", m.Repo, stdoutMessage, stderrMessage, err)
387405
err = nil
388406

services/mirror/mirror_test.go renamed to services/mirror/mirror_pull_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,31 @@ func Test_parseRemoteUpdateOutput(t *testing.T) {
6464
assert.Equal(t, "1c97ebc746", results[9].oldCommitID)
6565
assert.Equal(t, "976d27d52f", results[9].newCommitID)
6666
}
67+
68+
func Test_checkRecoverableSyncError(t *testing.T) {
69+
cases := []struct {
70+
recoverable bool
71+
message string
72+
}{
73+
// A race condition in http git-fetch where certain refs were listed on the remote and are no longer there, would exit status 128
74+
{true, "fatal: remote error: upload-pack: not our ref 988881adc9fc3655077dc2d4d757d480b5ea0e11"},
75+
// A race condition where a local gc/prune removes a named ref during a git-fetch would exit status 1
76+
{true, "cannot lock ref 'refs/pull/123456/merge': unable to resolve reference 'refs/pull/134153/merge'"},
77+
// A race condition in http git-fetch where named refs were listed on the remote and are no longer there
78+
{true, "error: cannot lock ref 'refs/remotes/origin/foo': unable to resolve reference 'refs/remotes/origin/foo': reference broken"},
79+
// A race condition in http git-fetch where named refs were force-pushed during the update, would exit status 128
80+
{true, "error: cannot lock ref 'refs/pull/123456/merge': is at 988881adc9fc3655077dc2d4d757d480b5ea0e11 but expected 7f894307ffc9553edbd0b671cab829786866f7b2"},
81+
// A race condition with other local git operations, such as git-maintenance, would exit status 128 (well, "Unable" the "U" is uppercase)
82+
{true, "fatal: Unable to create '/data/gitea-repositories/foo-org/bar-repo.git/./objects/info/commit-graphs/commit-graph-chain.lock': File exists."},
83+
// Missing or unauthorized credentials, would exit status 128
84+
{false, "fatal: Authentication failed for 'https://example.com/foo-does-not-exist/bar.git/'"},
85+
// A non-existent remote repository, would exit status 128
86+
{false, "fatal: Could not read from remote repository."},
87+
// A non-functioning proxy, would exit status 128
88+
{false, "fatal: unable to access 'https://example.com/foo-does-not-exist/bar.git/': Failed to connect to configured-https-proxy port 1080 after 0 ms: Couldn't connect to server"},
89+
}
90+
91+
for _, c := range cases {
92+
assert.Equal(t, c.recoverable, checkRecoverableSyncError(c.message), "test case: %s", c.message)
93+
}
94+
}

0 commit comments

Comments
 (0)