Skip to content

Commit a2b4ac6

Browse files
author
Bryan C. Mills
committed
cmd/go/internal/modfetch/codehost: add lockfiles for repos
The lockfile guards calls that may change the repo's filesystem contents. We don't know how robust VCS implementations are to running simultaneous commands, and this way we don't need to care: only one 'go' command at a time will modify any given repository. If we can guarantee that particular VCS implementations are robust enough across all of the VCS tool versions we support, we may be able to remove some of this locking to improve parallelism. Updates #26794 Change-Id: I578524974f5015629239cef43d3793aee2b9075c Reviewed-on: https://go-review.googlesource.com/c/146381 Run-TryBot: Bryan C. Mills <[email protected]> Reviewed-by: Russ Cox <[email protected]>
1 parent 143c1c8 commit a2b4ac6

File tree

3 files changed

+130
-32
lines changed

3 files changed

+130
-32
lines changed

Diff for: src/cmd/go/internal/modfetch/codehost/codehost.go

+29-9
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"time"
2121

2222
"cmd/go/internal/cfg"
23+
"cmd/go/internal/lockedfile"
2324
"cmd/go/internal/str"
2425
)
2526

@@ -131,9 +132,9 @@ var WorkRoot string
131132

132133
// WorkDir returns the name of the cached work directory to use for the
133134
// given repository type and name.
134-
func WorkDir(typ, name string) (string, error) {
135+
func WorkDir(typ, name string) (dir, lockfile string, err error) {
135136
if WorkRoot == "" {
136-
return "", fmt.Errorf("codehost.WorkRoot not set")
137+
return "", "", fmt.Errorf("codehost.WorkRoot not set")
137138
}
138139

139140
// We name the work directory for the SHA256 hash of the type and name.
@@ -142,22 +143,41 @@ func WorkDir(typ, name string) (string, error) {
142143
// that one checkout is never nested inside another. That nesting has
143144
// led to security problems in the past.
144145
if strings.Contains(typ, ":") {
145-
return "", fmt.Errorf("codehost.WorkDir: type cannot contain colon")
146+
return "", "", fmt.Errorf("codehost.WorkDir: type cannot contain colon")
146147
}
147148
key := typ + ":" + name
148-
dir := filepath.Join(WorkRoot, fmt.Sprintf("%x", sha256.Sum256([]byte(key))))
149+
dir = filepath.Join(WorkRoot, fmt.Sprintf("%x", sha256.Sum256([]byte(key))))
150+
151+
if cfg.BuildX {
152+
fmt.Fprintf(os.Stderr, "mkdir -p %s # %s %s\n", filepath.Dir(dir), typ, name)
153+
}
154+
if err := os.MkdirAll(filepath.Dir(dir), 0777); err != nil {
155+
return "", "", err
156+
}
157+
158+
lockfile = dir + ".lock"
159+
if cfg.BuildX {
160+
fmt.Fprintf(os.Stderr, "# lock %s", lockfile)
161+
}
162+
163+
unlock, err := lockedfile.MutexAt(lockfile).Lock()
164+
if err != nil {
165+
return "", "", fmt.Errorf("codehost.WorkDir: can't find or create lock file: %v", err)
166+
}
167+
defer unlock()
168+
149169
data, err := ioutil.ReadFile(dir + ".info")
150170
info, err2 := os.Stat(dir)
151171
if err == nil && err2 == nil && info.IsDir() {
152172
// Info file and directory both already exist: reuse.
153173
have := strings.TrimSuffix(string(data), "\n")
154174
if have != key {
155-
return "", fmt.Errorf("%s exists with wrong content (have %q want %q)", dir+".info", have, key)
175+
return "", "", fmt.Errorf("%s exists with wrong content (have %q want %q)", dir+".info", have, key)
156176
}
157177
if cfg.BuildX {
158178
fmt.Fprintf(os.Stderr, "# %s for %s %s\n", dir, typ, name)
159179
}
160-
return dir, nil
180+
return dir, lockfile, nil
161181
}
162182

163183
// Info file or directory missing. Start from scratch.
@@ -166,13 +186,13 @@ func WorkDir(typ, name string) (string, error) {
166186
}
167187
os.RemoveAll(dir)
168188
if err := os.MkdirAll(dir, 0777); err != nil {
169-
return "", err
189+
return "", "", err
170190
}
171191
if err := ioutil.WriteFile(dir+".info", []byte(key), 0666); err != nil {
172192
os.RemoveAll(dir)
173-
return "", err
193+
return "", "", err
174194
}
175-
return dir, nil
195+
return dir, lockfile, nil
176196
}
177197

178198
type RunError struct {

Diff for: src/cmd/go/internal/modfetch/codehost/git.go

+40-18
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"sync"
1818
"time"
1919

20+
"cmd/go/internal/lockedfile"
2021
"cmd/go/internal/par"
2122
)
2223

@@ -57,22 +58,29 @@ func newGitRepo(remote string, localOK bool) (Repo, error) {
5758
r := &gitRepo{remote: remote}
5859
if strings.Contains(remote, "://") {
5960
// This is a remote path.
60-
dir, err := WorkDir(gitWorkDirType, r.remote)
61+
var err error
62+
r.dir, r.mu.Path, err = WorkDir(gitWorkDirType, r.remote)
6163
if err != nil {
6264
return nil, err
6365
}
64-
r.dir = dir
65-
if _, err := os.Stat(filepath.Join(dir, "objects")); err != nil {
66-
if _, err := Run(dir, "git", "init", "--bare"); err != nil {
67-
os.RemoveAll(dir)
66+
67+
unlock, err := r.mu.Lock()
68+
if err != nil {
69+
return nil, err
70+
}
71+
defer unlock()
72+
73+
if _, err := os.Stat(filepath.Join(r.dir, "objects")); err != nil {
74+
if _, err := Run(r.dir, "git", "init", "--bare"); err != nil {
75+
os.RemoveAll(r.dir)
6876
return nil, err
6977
}
7078
// We could just say git fetch https://whatever later,
7179
// but this lets us say git fetch origin instead, which
7280
// is a little nicer. More importantly, using a named remote
7381
// avoids a problem with Git LFS. See golang.org/issue/25605.
74-
if _, err := Run(dir, "git", "remote", "add", "origin", r.remote); err != nil {
75-
os.RemoveAll(dir)
82+
if _, err := Run(r.dir, "git", "remote", "add", "origin", r.remote); err != nil {
83+
os.RemoveAll(r.dir)
7684
return nil, err
7785
}
7886
r.remote = "origin"
@@ -97,6 +105,7 @@ func newGitRepo(remote string, localOK bool) (Repo, error) {
97105
return nil, fmt.Errorf("%s exists but is not a directory", remote)
98106
}
99107
r.dir = remote
108+
r.mu.Path = r.dir + ".lock"
100109
}
101110
return r, nil
102111
}
@@ -106,7 +115,8 @@ type gitRepo struct {
106115
local bool
107116
dir string
108117

109-
mu sync.Mutex // protects fetchLevel, some git repo state
118+
mu lockedfile.Mutex // protects fetchLevel and git repo state
119+
110120
fetchLevel int
111121

112122
statCache par.Cache
@@ -304,11 +314,11 @@ func (r *gitRepo) stat(rev string) (*RevInfo, error) {
304314
}
305315

306316
// Protect r.fetchLevel and the "fetch more and more" sequence.
307-
// TODO(rsc): Add LockDir and use it for protecting that
308-
// sequence, so that multiple processes don't collide in their
309-
// git commands.
310-
r.mu.Lock()
311-
defer r.mu.Unlock()
317+
unlock, err := r.mu.Lock()
318+
if err != nil {
319+
return nil, err
320+
}
321+
defer unlock()
312322

313323
// Perhaps r.localTags did not have the ref when we loaded local tags,
314324
// but we've since done fetches that pulled down the hash we need
@@ -495,8 +505,11 @@ func (r *gitRepo) ReadFileRevs(revs []string, file string, maxSize int64) (map[s
495505

496506
// Protect r.fetchLevel and the "fetch more and more" sequence.
497507
// See stat method above.
498-
r.mu.Lock()
499-
defer r.mu.Unlock()
508+
unlock, err := r.mu.Lock()
509+
if err != nil {
510+
return nil, err
511+
}
512+
defer unlock()
500513

501514
var refs []string
502515
var protoFlag []string
@@ -658,8 +671,11 @@ func (r *gitRepo) RecentTag(rev, prefix string) (tag string, err error) {
658671
// There are plausible tags, but we don't know if rev is a descendent of any of them.
659672
// Fetch the history to find out.
660673

661-
r.mu.Lock()
662-
defer r.mu.Unlock()
674+
unlock, err := r.mu.Lock()
675+
if err != nil {
676+
return "", err
677+
}
678+
defer unlock()
663679

664680
if r.fetchLevel < fetchAll {
665681
// Fetch all heads and tags and see if that gives us enough history.
@@ -678,7 +694,7 @@ func (r *gitRepo) RecentTag(rev, prefix string) (tag string, err error) {
678694
// unreachable for a reason).
679695
//
680696
// Try one last time in case some other goroutine fetched rev while we were
681-
// waiting on r.mu.
697+
// waiting on the lock.
682698
describe()
683699
return tag, err
684700
}
@@ -694,6 +710,12 @@ func (r *gitRepo) ReadZip(rev, subdir string, maxSize int64) (zip io.ReadCloser,
694710
return nil, "", err
695711
}
696712

713+
unlock, err := r.mu.Lock()
714+
if err != nil {
715+
return nil, "", err
716+
}
717+
defer unlock()
718+
697719
if err := ensureGitAttributes(r.dir); err != nil {
698720
return nil, "", err
699721
}

Diff for: src/cmd/go/internal/modfetch/codehost/vcs.go

+61-5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"sync"
1919
"time"
2020

21+
"cmd/go/internal/lockedfile"
2122
"cmd/go/internal/par"
2223
"cmd/go/internal/str"
2324
)
@@ -56,6 +57,8 @@ func NewRepo(vcs, remote string) (Repo, error) {
5657
var vcsRepoCache par.Cache
5758

5859
type vcsRepo struct {
60+
mu lockedfile.Mutex // protects all commands, so we don't have to decide which are safe on a per-VCS basis
61+
5962
remote string
6063
cmd *vcsCmd
6164
dir string
@@ -81,18 +84,27 @@ func newVCSRepo(vcs, remote string) (Repo, error) {
8184
if !strings.Contains(remote, "://") {
8285
return nil, fmt.Errorf("invalid vcs remote: %s %s", vcs, remote)
8386
}
87+
8488
r := &vcsRepo{remote: remote, cmd: cmd}
89+
var err error
90+
r.dir, r.mu.Path, err = WorkDir(vcsWorkDirType+vcs, r.remote)
91+
if err != nil {
92+
return nil, err
93+
}
94+
8595
if cmd.init == nil {
8696
return r, nil
8797
}
88-
dir, err := WorkDir(vcsWorkDirType+vcs, r.remote)
98+
99+
unlock, err := r.mu.Lock()
89100
if err != nil {
90101
return nil, err
91102
}
92-
r.dir = dir
93-
if _, err := os.Stat(filepath.Join(dir, "."+vcs)); err != nil {
94-
if _, err := Run(dir, cmd.init(r.remote)); err != nil {
95-
os.RemoveAll(dir)
103+
defer unlock()
104+
105+
if _, err := os.Stat(filepath.Join(r.dir, "."+vcs)); err != nil {
106+
if _, err := Run(r.dir, cmd.init(r.remote)); err != nil {
107+
os.RemoveAll(r.dir)
96108
return nil, err
97109
}
98110
}
@@ -270,6 +282,12 @@ func (r *vcsRepo) loadBranches() {
270282
}
271283

272284
func (r *vcsRepo) Tags(prefix string) ([]string, error) {
285+
unlock, err := r.mu.Lock()
286+
if err != nil {
287+
return nil, err
288+
}
289+
defer unlock()
290+
273291
r.tagsOnce.Do(r.loadTags)
274292

275293
tags := []string{}
@@ -283,6 +301,12 @@ func (r *vcsRepo) Tags(prefix string) ([]string, error) {
283301
}
284302

285303
func (r *vcsRepo) Stat(rev string) (*RevInfo, error) {
304+
unlock, err := r.mu.Lock()
305+
if err != nil {
306+
return nil, err
307+
}
308+
defer unlock()
309+
286310
if rev == "latest" {
287311
rev = r.cmd.latest
288312
}
@@ -332,6 +356,14 @@ func (r *vcsRepo) ReadFile(rev, file string, maxSize int64) ([]byte, error) {
332356
if err != nil {
333357
return nil, err
334358
}
359+
360+
// r.Stat acquires r.mu, so lock after that.
361+
unlock, err := r.mu.Lock()
362+
if err != nil {
363+
return nil, err
364+
}
365+
defer unlock()
366+
335367
out, err := Run(r.dir, r.cmd.readFile(rev, file, r.remote))
336368
if err != nil {
337369
return nil, os.ErrNotExist
@@ -340,14 +372,38 @@ func (r *vcsRepo) ReadFile(rev, file string, maxSize int64) ([]byte, error) {
340372
}
341373

342374
func (r *vcsRepo) ReadFileRevs(revs []string, file string, maxSize int64) (map[string]*FileRev, error) {
375+
// We don't technically need to lock here since we're returning an error
376+
// uncondititonally, but doing so anyway will help to avoid baking in
377+
// lock-inversion bugs.
378+
unlock, err := r.mu.Lock()
379+
if err != nil {
380+
return nil, err
381+
}
382+
defer unlock()
383+
343384
return nil, fmt.Errorf("ReadFileRevs not implemented")
344385
}
345386

346387
func (r *vcsRepo) RecentTag(rev, prefix string) (tag string, err error) {
388+
// We don't technically need to lock here since we're returning an error
389+
// uncondititonally, but doing so anyway will help to avoid baking in
390+
// lock-inversion bugs.
391+
unlock, err := r.mu.Lock()
392+
if err != nil {
393+
return "", err
394+
}
395+
defer unlock()
396+
347397
return "", fmt.Errorf("RecentTags not implemented")
348398
}
349399

350400
func (r *vcsRepo) ReadZip(rev, subdir string, maxSize int64) (zip io.ReadCloser, actualSubdir string, err error) {
401+
unlock, err := r.mu.Lock()
402+
if err != nil {
403+
return nil, "", err
404+
}
405+
defer unlock()
406+
351407
if rev == "latest" {
352408
rev = r.cmd.latest
353409
}

0 commit comments

Comments
 (0)