diff --git a/cli/internal/fs/path.go b/cli/internal/fs/path.go index 49b2f2008cd0e..843bbeb7a6fec 100644 --- a/cli/internal/fs/path.go +++ b/cli/internal/fs/path.go @@ -120,6 +120,11 @@ func (ap AbsolutePath) Remove() error { return os.Remove(ap.asString()) } +// RenameTo implements os.Rename for an absolute path +func (ap AbsolutePath) RenameTo(target AbsolutePath) error { + return os.Rename(ap.asString(), target.asString()) +} + // GetVolumeRoot returns the root directory given an absolute path. func GetVolumeRoot(absolutePath string) string { return filepath.VolumeName(absolutePath) + string(os.PathSeparator) diff --git a/cli/internal/runcache/runcache.go b/cli/internal/runcache/runcache.go index 616690cbb7009..0588cd1c854ca 100644 --- a/cli/internal/runcache/runcache.go +++ b/cli/internal/runcache/runcache.go @@ -155,7 +155,8 @@ type TaskCache struct { hash string pt *nodes.PackageTask cachingDisabled bool - LogFileName fs.AbsolutePath + targetLogFileName fs.AbsolutePath + tempLogFileName fs.AbsolutePath } // RestoreOutputs attempts to restore output for the corresponding task from the cache. Returns true @@ -182,11 +183,11 @@ func (tc TaskCache) RestoreOutputs(terminal *cli.PrefixedUi, logger hclog.Logger case HashLogs: terminal.Output(fmt.Sprintf("cache hit, suppressing output %s", ui.Dim(tc.hash))) case FullLogs: - logger.Debug("log file", "path", tc.LogFileName) - if tc.LogFileName.FileExists() { + logger.Debug("log file", "path", tc.targetLogFileName) + if tc.targetLogFileName.FileExists() { // The task label is baked into the log file, so we need to grab the underlying Ui // instance in order to not duplicate it - tc.rc.logReplayer(logger, terminal.Ui, tc.LogFileName) + tc.rc.logReplayer(logger, terminal.Ui, tc.targetLogFileName) } default: // NoLogs, do not output anything @@ -201,17 +202,35 @@ type nopWriteCloser struct { func (nopWriteCloser) Close() error { return nil } -type fileWriterCloser struct { +// renamingCloser flushes, closes, and renames the given file +// to the target log file in the TaskCache. +// +// Log files are hard-linked into the cache, so when re-running a task +// we need to ensure we don't pollute a previous run. We do this by logging +// to a temp file and then performing this rename to the actual log file. +// Note that: +// - renames will succeed even if the target already exists, as long as the target isn't a directory. +// - All of our targets are log files +// - renames can fail if the rename is across filesystem boundaries. +// - Our renames are within the same directory. +type renamingCloser struct { io.Writer file *os.File + tc *TaskCache bufio *bufio.Writer } -func (fwc *fileWriterCloser) Close() error { +func (fwc *renamingCloser) Close() error { if err := fwc.bufio.Flush(); err != nil { return err } - return fwc.file.Close() + if err := fwc.file.Close(); err != nil { + return err + } + if err := fwc.tc.tempLogFileName.RenameTo(fwc.tc.targetLogFileName); err != nil { + return err + } + return nil } // OutputWriter creates a sink suitable for handling the output of the command associated @@ -220,11 +239,11 @@ func (tc TaskCache) OutputWriter() (io.WriteCloser, error) { if tc.cachingDisabled || tc.rc.writesDisabled { return nopWriteCloser{os.Stdout}, nil } - // Setup log file - if err := tc.LogFileName.EnsureDir(); err != nil { + // Setup the temp log file + if err := tc.tempLogFileName.EnsureDir(); err != nil { return nil, err } - output, err := tc.LogFileName.Create() + output, err := tc.tempLogFileName.Create() if err != nil { return nil, err } @@ -237,9 +256,10 @@ func (tc TaskCache) OutputWriter() (io.WriteCloser, error) { _ = output.Close() return nil, err } - fwc := &fileWriterCloser{ + fwc := &renamingCloser{ file: output, bufio: bufWriter, + tc: &tc, } if tc.rc.cacheMissLogsMode == NoLogs || tc.rc.cacheMissLogsMode == HashLogs { // only write to log file, not to stdout @@ -283,7 +303,10 @@ func (tc TaskCache) SaveOutputs(logger hclog.Logger, terminal cli.Ui, duration i // TaskCache returns a TaskCache instance, providing an interface to the underlying cache specific // to this run and the given PackageTask func (rc *RunCache) TaskCache(pt *nodes.PackageTask, hash string) TaskCache { - logFileName := rc.repoRoot.Join(pt.RepoRelativeLogFile()) + repoRelativeLogFile := pt.RepoRelativeLogFile() + repoRelativeTempLogFile := repoRelativeLogFile + ".tmp" + logFileName := rc.repoRoot.Join(repoRelativeLogFile) + tempLogFileName := rc.repoRoot.Join(repoRelativeTempLogFile) hashableOutputs := pt.HashableOutputs() repoRelativeGlobs := make([]string, len(hashableOutputs)) for index, output := range hashableOutputs { @@ -295,7 +318,8 @@ func (rc *RunCache) TaskCache(pt *nodes.PackageTask, hash string) TaskCache { hash: hash, pt: pt, cachingDisabled: !pt.TaskDefinition.ShouldCache, - LogFileName: logFileName, + targetLogFileName: logFileName, + tempLogFileName: tempLogFileName, } }