Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 19 additions & 12 deletions internal/gitclone/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,14 +320,26 @@ func WithReadLockReturn[T any](repo *Repository, fn func() (T, error)) (T, error
return fn()
}

// MarkRestored configures a restored snapshot (e.g. from S3) as a mirror and
// leaves the repository in StateCloning. The caller must call MarkReady after
// a successful catch-up fetch to transition to StateReady. While in
// StateCloning, requests are proxied to upstream instead of served from the
// potentially-stale local mirror.
func (r *Repository) MarkRestored(ctx context.Context) error {
// TryStartCloning atomically transitions the repository from StateEmpty to
// StateCloning. Returns true if this goroutine won the transition and should
// proceed with the clone/restore; false if another goroutine already claimed it.
func (r *Repository) TryStartCloning() bool {
r.mu.Lock()
defer r.mu.Unlock()
if r.state != StateEmpty {
return false
}
r.state = StateCloning
return true
}

// MarkRestored configures a restored snapshot (e.g. from S3) as a mirror.
// The caller must have already transitioned to StateCloning (via
// TryStartCloning) before extracting the snapshot. On error the state is
// left as StateCloning so the caller can fall back to a fresh clone.
func (r *Repository) MarkRestored(ctx context.Context) error {
r.mu.Lock()
if r.state == StateReady {
r.mu.Unlock()
return nil
}
Expand All @@ -338,14 +350,9 @@ func (r *Repository) MarkRestored(ctx context.Context) error {
if err == nil && r.config.Maintenance {
err = registerMaintenance(ctx, r.path)
}

r.mu.Lock()
if err != nil {
r.state = StateEmpty
r.mu.Unlock()
return errors.Wrap(err, "configure mirror after restore")
}
r.mu.Unlock()
return nil
}

Expand All @@ -360,7 +367,7 @@ func (r *Repository) MarkReady() {

func (r *Repository) Clone(ctx context.Context) error {
r.mu.Lock()
if r.state != StateEmpty {
if r.state == StateReady {
r.mu.Unlock()
return nil
}
Expand Down
8 changes: 8 additions & 0 deletions internal/strategy/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,14 @@ func (s *Strategy) ensureCloneReady(ctx context.Context, repo *gitclone.Reposito
}

func (s *Strategy) startClone(ctx context.Context, repo *gitclone.Repository) {
// Atomically claim the clone so only one goroutine performs the restore
// or clone. Without this gate, concurrent snapshot requests each call
// startClone and extract tarballs over the same directory, corrupting
// packed-refs and other git metadata.
if !repo.TryStartCloning() {
return
}

logger := logging.FromContext(ctx)
upstream := repo.UpstreamURL()

Expand Down