internal/git/objectpool/link.go (151 lines of code) (raw):
package objectpool
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"gitlab.com/gitlab-org/gitaly/v16/internal/git/localrepo"
"gitlab.com/gitlab-org/gitaly/v16/internal/git/stats"
"gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/storage"
"gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/transaction"
"gitlab.com/gitlab-org/gitaly/v16/internal/safe"
"gitlab.com/gitlab-org/gitaly/v16/internal/transaction/voting"
)
// Link calls the non-receiver method version of Link with the parameters
// injected from the object pool.
func (o *ObjectPool) Link(ctx context.Context, repo *localrepo.Repo) error {
return Link(ctx, o.Repo, repo, o.txManager)
}
// Link will link the given repository to the object pool. This is done by writing the object pool's
// path relative to the repository into the repository's "alternates" file. This does not trigger
// deduplication, which is the responsibility of the caller.
func Link(ctx context.Context, pool, repo *localrepo.Repo, txManager transaction.Manager) (returnedErr error) {
altPath, err := repo.InfoAlternatesPath(ctx)
if err != nil {
return err
}
expectedRelPath, err := getRelativeObjectPath(ctx, pool, repo)
if err != nil {
return err
}
linked, err := linkedToRepository(ctx, pool, repo)
if err != nil {
return err
}
if linked {
// When the repository is already linked to the repository, cast a vote to ensure the
// repository is consistent with the other replicas.
if err := transaction.VoteOnContext(ctx, txManager, voting.VoteFromData([]byte("repository linked")), voting.Committed); err != nil {
return fmt.Errorf("vote on linked repository: %w", err)
}
return nil
}
alternatesWriter, err := safe.NewLockingFileWriter(altPath)
if err != nil {
return fmt.Errorf("creating alternates writer: %w", err)
}
defer func() {
if err := alternatesWriter.Close(); err != nil && returnedErr == nil {
returnedErr = fmt.Errorf("closing alternates writer: %w", err)
}
}()
if _, err := io.WriteString(alternatesWriter, expectedRelPath); err != nil {
return fmt.Errorf("writing alternates: %w", err)
}
if err := transaction.CommitLockedFile(ctx, txManager, alternatesWriter); err != nil {
return fmt.Errorf("committing alternates: %w", err)
}
if tx := storage.ExtractTransaction(ctx); tx != nil {
alternatesRelativePath, err := filepath.Rel(tx.FS().Root(), altPath)
if err != nil {
return fmt.Errorf("rel alternates file: %w", err)
}
if err := tx.FS().RecordFile(alternatesRelativePath); err != nil {
return fmt.Errorf("record alternates file")
}
}
return removeMemberBitmaps(ctx, pool, repo)
}
// removeMemberBitmaps removes packfile bitmaps from the member
// repository that just joined the pool. If Git finds two packfiles with
// bitmaps it will print a warning, which is visible to the end user
// during a Git clone. Our goal is to avoid that warning. In normal
// operation, the next 'git gc' or 'git repack -ad' on the member
// repository will remove its local bitmap file. In other words the
// situation we eventually converge to is that the pool may have a bitmap
// but none of its members will. With removeMemberBitmaps we try to
// change "eventually" to "immediately", so that users won't see the
// warning. https://gitlab.com/gitlab-org/gitaly/issues/1728
func removeMemberBitmaps(ctx context.Context, pool *localrepo.Repo, repo *localrepo.Repo) error {
poolPath, err := pool.Path(ctx)
if err != nil {
return err
}
poolBitmaps, err := getBitmaps(poolPath)
if err != nil {
return err
}
if len(poolBitmaps) == 0 {
return nil
}
repoPath, err := repo.Path(ctx)
if err != nil {
return err
}
memberBitmaps, err := getBitmaps(repoPath)
if err != nil {
return err
}
if len(memberBitmaps) == 0 {
return nil
}
for _, bitmap := range memberBitmaps {
if err := os.Remove(bitmap); err != nil && !os.IsNotExist(err) {
return err
}
}
return nil
}
func getBitmaps(repoPath string) ([]string, error) {
packDir := filepath.Join(repoPath, "objects/pack")
entries, err := os.ReadDir(packDir)
if err != nil {
return nil, err
}
var bitmaps []string
for _, entry := range entries {
if name := entry.Name(); strings.HasSuffix(name, ".bitmap") && strings.HasPrefix(name, "pack-") {
bitmaps = append(bitmaps, filepath.Join(packDir, name))
}
}
return bitmaps, nil
}
func getRelativeObjectPath(ctx context.Context, pool, repo *localrepo.Repo) (string, error) {
poolPath, err := pool.Path(ctx)
if err != nil {
return "", fmt.Errorf("getting object pool path: %w", err)
}
repoPath, err := repo.Path(ctx)
if err != nil {
return "", fmt.Errorf("getting repository path: %w", err)
}
relPath, err := filepath.Rel(filepath.Join(repoPath, "objects"), poolPath)
if err != nil {
return "", err
}
return filepath.Join(relPath, "objects"), nil
}
// linkedToRepository tests if a repository is linked to an object pool
func linkedToRepository(ctx context.Context, pool, repo *localrepo.Repo) (bool, error) {
poolPath, err := pool.Path(ctx)
if err != nil {
return false, fmt.Errorf("getting object pool path: %w", err)
}
repoPath, err := repo.Path(ctx)
if err != nil {
return false, fmt.Errorf("getting repo path: %w", err)
}
altInfo, err := stats.AlternatesInfoForRepository(repoPath)
if err != nil {
return false, fmt.Errorf("getting alternates info: %w", err)
}
if !altInfo.Exists || len(altInfo.ObjectDirectories) == 0 {
return false, nil
}
relPath := altInfo.ObjectDirectories[0]
expectedRelPath, err := getRelativeObjectPath(ctx, pool, repo)
if err != nil {
return false, err
}
if relPath == expectedRelPath {
return true, nil
}
if filepath.Clean(relPath) != filepath.Join(poolPath, "objects") {
return false, fmt.Errorf("unexpected alternates content: %q", relPath)
}
return false, nil
}