Skip to content

Commit f3d572d

Browse files
adonovangopherbot
authored andcommitted
cmd/go: fix race applying fixes in fix and vet -fix modes
Previously, the cmd/fix tool, which is analogous to a compiler in a "go fix" or "go vet -fix" build, applied its fixes directly to source files during the build. However, this led to races since the edits may in some cases occur concurrently with other build steps that are still reading those source file. This change separates the computation of the fixes, which happens during the build, and applying the fixes, which happens in a phase after the build. The unitchecker now accepts a FixArchive file name (see CL 726940). If it is non-empty, the unitchecker will write the fixed files into an archive instead of updating them directly. The vet build sets this option, then reads the produced zip files in the second phase. The files are saved in the cache; some care is required to sequence the various cache operations so that a cache hit has all-or-nothing semantics. The tweak to vet_basic.txt is a sign that there was a latent bug, inadvertently fixed by this change: because the old approach relied on side effects of cmd/fix to mutate files, rather than the current pure-functional approach of computing fixes which are then applied as a second pass, a cache hit would cause some edits not to be applied. Now they are applied. Fixes #71859 Change-Id: Ib8e70644ec246dcdb20a90794c11ea6fd420247d Reviewed-on: https://go-review.googlesource.com/c/go/+/727000 Auto-Submit: Alan Donovan <adonovan@google.com> Reviewed-by: Michael Matloob <matloob@golang.org> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Michael Matloob <matloob@google.com>
1 parent 7634553 commit f3d572d

File tree

5 files changed

+141
-31
lines changed

5 files changed

+141
-31
lines changed

src/cmd/go/internal/test/test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1371,7 +1371,7 @@ func addTestVet(loaderstate *modload.State, b *work.Builder, p *load.Package, ru
13711371
return
13721372
}
13731373

1374-
vet := b.VetAction(loaderstate, work.ModeBuild, work.ModeBuild, p)
1374+
vet := b.VetAction(loaderstate, work.ModeBuild, work.ModeBuild, false, p)
13751375
runAction.Deps = append(runAction.Deps, vet)
13761376
// Install will clean the build directory.
13771377
// Make sure vet runs first.

src/cmd/go/internal/vet/vet.go

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
package vet
77

88
import (
9+
"archive/zip"
10+
"bytes"
911
"context"
1012
"encoding/json"
1113
"errors"
@@ -176,6 +178,7 @@ func run(ctx context.Context, cmd *base.Command, args []string) {
176178

177179
work.VetExplicit = len(toolFlags) > 0
178180

181+
applyFixes := false
179182
if cmd.Name() == "fix" || *vetFixFlag {
180183
// fix mode: 'go fix' or 'go vet -fix'
181184
if jsonFlag {
@@ -186,6 +189,8 @@ func run(ctx context.Context, cmd *base.Command, args []string) {
186189
toolFlags = append(toolFlags, "-fix")
187190
if diffFlag {
188191
toolFlags = append(toolFlags, "-diff")
192+
} else {
193+
applyFixes = true
189194
}
190195
}
191196
if contextFlag != -1 {
@@ -226,13 +231,21 @@ func run(ctx context.Context, cmd *base.Command, args []string) {
226231
base.Fatalf("no packages to %s", cmd.Name())
227232
}
228233

234+
// Build action graph.
229235
b := work.NewBuilder("", moduleLoaderState.VendorDirOrEmpty)
230236
defer func() {
231237
if err := b.Close(); err != nil {
232238
base.Fatal(err)
233239
}
234240
}()
235241

242+
root := &work.Action{Mode: "go " + cmd.Name()}
243+
244+
addVetAction := func(p *load.Package) {
245+
act := b.VetAction(moduleLoaderState, work.ModeBuild, work.ModeBuild, applyFixes, p)
246+
root.Deps = append(root.Deps, act)
247+
}
248+
236249
// To avoid file corruption from duplicate application of
237250
// fixes (in fix mode), and duplicate reporting of diagnostics
238251
// (in vet mode), we must run the tool only once for each
@@ -248,8 +261,6 @@ func run(ctx context.Context, cmd *base.Command, args []string) {
248261
// We needn't worry about intermediate test variants, as they
249262
// will only be executed in VetxOnly mode, for facts but not
250263
// diagnostics.
251-
252-
root := &work.Action{Mode: "go " + cmd.Name()}
253264
for _, p := range pkgs {
254265
_, ptest, pxtest, perr := load.TestPackagesFor(moduleLoaderState, ctx, pkgOpts, p, nil)
255266
if perr != nil {
@@ -262,13 +273,65 @@ func run(ctx context.Context, cmd *base.Command, args []string) {
262273
}
263274
if len(ptest.GoFiles) > 0 || len(ptest.CgoFiles) > 0 {
264275
// The test package includes all the files of primary package.
265-
root.Deps = append(root.Deps, b.VetAction(moduleLoaderState, work.ModeBuild, work.ModeBuild, ptest))
276+
addVetAction(ptest)
266277
}
267278
if pxtest != nil {
268-
root.Deps = append(root.Deps, b.VetAction(moduleLoaderState, work.ModeBuild, work.ModeBuild, pxtest))
279+
addVetAction(pxtest)
269280
}
270281
}
271282
b.Do(ctx, root)
283+
284+
// Apply fixes.
285+
//
286+
// We do this as a separate phase after the build to avoid
287+
// races between source file updates and reads of those same
288+
// files by concurrent actions of the ongoing build.
289+
//
290+
// If a file is fixed by multiple actions, they must be consistent.
291+
if applyFixes {
292+
contents := make(map[string][]byte)
293+
// Gather the fixes.
294+
for _, act := range root.Deps {
295+
if act.FixArchive != "" {
296+
if err := readZip(act.FixArchive, contents); err != nil {
297+
base.Errorf("reading archive of fixes: %v", err)
298+
return
299+
}
300+
}
301+
}
302+
// Apply them.
303+
for filename, content := range contents {
304+
if err := os.WriteFile(filename, content, 0644); err != nil {
305+
base.Errorf("applying fix: %v", err)
306+
}
307+
}
308+
}
309+
}
310+
311+
// readZip reads the zipfile entries into the provided map.
312+
// It reports an error if updating the map would change an existing entry.
313+
func readZip(zipfile string, out map[string][]byte) error {
314+
r, err := zip.OpenReader(zipfile)
315+
if err != nil {
316+
return err
317+
}
318+
defer r.Close() // ignore error
319+
for _, f := range r.File {
320+
rc, err := f.Open()
321+
if err != nil {
322+
return err
323+
}
324+
content, err := io.ReadAll(rc)
325+
rc.Close() // ignore error
326+
if err != nil {
327+
return err
328+
}
329+
if prev, ok := out[f.Name]; ok && !bytes.Equal(prev, content) {
330+
return fmt.Errorf("inconsistent fixes to file %v", f.Name)
331+
}
332+
out[f.Name] = content
333+
}
334+
return nil
272335
}
273336

274337
// printJSONDiagnostics parses JSON (from the tool's stdout) and

src/cmd/go/internal/work/action.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,13 @@ type Action struct {
109109
actionID cache.ActionID // cache ID of action input
110110
buildID string // build ID of action output
111111

112-
VetxOnly bool // Mode=="vet": only being called to supply info about dependencies
113-
needVet bool // Mode=="build": need to fill in vet config
114-
needBuild bool // Mode=="build": need to do actual build (can be false if needVet is true)
115-
vetCfg *vetConfig // vet config
116-
output []byte // output redirect buffer (nil means use b.Print)
112+
VetxOnly bool // Mode=="vet": only being called to supply info about dependencies
113+
needVet bool // Mode=="build": need to fill in vet config
114+
needBuild bool // Mode=="build": need to do actual build (can be false if needVet is true)
115+
needFix bool // Mode=="vet": need secondary target, a .zip file containing fixes
116+
vetCfg *vetConfig // vet config
117+
FixArchive string // the created .zip file containing fixes (if needFix)
118+
output []byte // output redirect buffer (nil means use b.Print)
117119

118120
sh *Shell // lazily created per-Action shell; see Builder.Shell
119121

@@ -869,9 +871,10 @@ func (b *Builder) cgoAction(p *load.Package, objdir string, deps []*Action, hasC
869871
// It depends on the action for compiling p.
870872
// If the caller may be causing p to be installed, it is up to the caller
871873
// to make sure that the install depends on (runs after) vet.
872-
func (b *Builder) VetAction(s *modload.State, mode, depMode BuildMode, p *load.Package) *Action {
874+
func (b *Builder) VetAction(s *modload.State, mode, depMode BuildMode, needFix bool, p *load.Package) *Action {
873875
a := b.vetAction(s, mode, depMode, p)
874876
a.VetxOnly = false
877+
a.needFix = needFix
875878
return a
876879
}
877880

src/cmd/go/internal/work/exec.go

Lines changed: 63 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,6 +1187,7 @@ type vetConfig struct {
11871187
VetxOutput string // write vetx data to this output file
11881188
Stdout string // write stdout (JSON, unified diff) to this output file
11891189
GoVersion string // Go version for package
1190+
FixArchive string // write fixed files to this zip archive, if non-empty
11901191

11911192
SucceedOnTypecheckFailure bool // awful hack; see #18395 and below
11921193
}
@@ -1308,6 +1309,9 @@ func (b *Builder) vet(ctx context.Context, a *Action) error {
13081309
vcfg.VetxOnly = a.VetxOnly
13091310
vcfg.VetxOutput = a.Objdir + "vet.out"
13101311
vcfg.Stdout = a.Objdir + "vet.stdout"
1312+
if a.needFix {
1313+
vcfg.FixArchive = a.Objdir + "vet.fix.zip"
1314+
}
13111315
vcfg.PackageVetx = make(map[string]string)
13121316

13131317
h := cache.NewHash("vet " + a.Package.ImportPath)
@@ -1368,31 +1372,60 @@ func (b *Builder) vet(ctx context.Context, a *Action) error {
13681372
vcfg.PackageVetx[a1.Package.ImportPath] = a1.built
13691373
}
13701374
}
1371-
vetxKey := cache.ActionID(h.Sum()) // for .vetx file
1372-
1373-
fmt.Fprintf(h, "stdout\n")
1374-
stdoutKey := cache.ActionID(h.Sum()) // for .stdout file
1375+
var (
1376+
id = cache.ActionID(h.Sum()) // for .vetx file
1377+
stdoutKey = cache.Subkey(id, "stdout") // for .stdout file
1378+
fixArchiveKey = cache.Subkey(id, "fix.zip") // for .fix.zip file
1379+
)
13751380

13761381
// Check the cache; -a forces a rebuild.
13771382
if !cfg.BuildA {
13781383
c := cache.Default()
1379-
if vcfg.VetxOnly {
1380-
if file, _, err := cache.GetFile(c, vetxKey); err == nil {
1381-
a.built = file
1382-
return nil
1384+
1385+
// There may be multiple artifacts in the cache.
1386+
// We need to retrieve them all, or none:
1387+
// the effect must be transactional.
1388+
var (
1389+
vetxFile string // name of cached .vetx file
1390+
fixArchive string // name of cached .fix.zip file
1391+
stdout io.Reader = bytes.NewReader(nil) // cached stdout stream
1392+
)
1393+
1394+
// Obtain location of cached .vetx file.
1395+
vetxFile, _, err := cache.GetFile(c, id)
1396+
if err != nil {
1397+
goto cachemiss
1398+
}
1399+
1400+
// Obtain location of cached .fix.zip file (if needed).
1401+
if a.needFix {
1402+
file, _, err := cache.GetFile(c, fixArchiveKey)
1403+
if err != nil {
1404+
goto cachemiss
13831405
}
1384-
} else {
1385-
// Copy cached vet.std files to stdout.
1386-
if file, _, err := cache.GetFile(c, stdoutKey); err == nil {
1387-
f, err := os.Open(file)
1388-
if err != nil {
1389-
return err
1390-
}
1391-
defer f.Close() // ignore error (can't fail)
1392-
return VetHandleStdout(f)
1406+
fixArchive = file
1407+
}
1408+
1409+
// Copy cached .stdout file to stdout.
1410+
if file, _, err := cache.GetFile(c, stdoutKey); err == nil {
1411+
f, err := os.Open(file)
1412+
if err != nil {
1413+
goto cachemiss
13931414
}
1415+
defer f.Close() // ignore error (can't fail)
1416+
stdout = f
1417+
}
1418+
1419+
// Cache hit: commit transaction.
1420+
a.built = vetxFile
1421+
a.FixArchive = fixArchive
1422+
if err := VetHandleStdout(stdout); err != nil {
1423+
return err // internal error (don't fall through to cachemiss)
13941424
}
1425+
1426+
return nil
13951427
}
1428+
cachemiss:
13961429

13971430
js, err := json.MarshalIndent(vcfg, "", "\t")
13981431
if err != nil {
@@ -1419,13 +1452,23 @@ func (b *Builder) vet(ctx context.Context, a *Action) error {
14191452
return err
14201453
}
14211454

1422-
// Vet tool succeeded, possibly with facts and JSON stdout. Save both in cache.
1455+
// Vet tool succeeded, possibly with facts, fixes, or JSON stdout.
1456+
// Save all in cache.
14231457

1424-
// Save facts
1458+
// Save facts.
14251459
if f, err := os.Open(vcfg.VetxOutput); err == nil {
14261460
defer f.Close() // ignore error
14271461
a.built = vcfg.VetxOutput
1428-
cache.Default().Put(vetxKey, f) // ignore error
1462+
cache.Default().Put(id, f) // ignore error
1463+
}
1464+
1465+
// Save fix archive (if any).
1466+
if a.needFix {
1467+
if f, err := os.Open(vcfg.FixArchive); err == nil {
1468+
defer f.Close() // ignore error
1469+
a.FixArchive = vcfg.FixArchive
1470+
cache.Default().Put(fixArchiveKey, f) // ignore error
1471+
}
14291472
}
14301473

14311474
// Save stdout.

src/cmd/go/testdata/script/vet_basic.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ stderr '-json and -diff cannot be used together'
9191
# Legacy way of selecting fixers is a no-op.
9292
go fix -fix=old1,old2 example.com/x
9393
stderr 'go fix: the -fix=old1,old2 flag is obsolete and has no effect'
94+
cp x.go.bak x.go
9495

9596
# -c=n flag shows n lines of context.
9697
! go vet -c=2 -printf example.com/x

0 commit comments

Comments
 (0)