summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2021-10-17 11:54:55 +0200
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2021-10-18 12:13:13 +0200
commitba35e69856900b6fc92681aa841cdcaefbb4b121 (patch)
treeb9ffc699a99b2d6d947e1d53c383a1352ce93980
parentc7957c90e83ff2b2cc958bd61486a244f0fd8891 (diff)
Add a cross process build lock and use it in the archetype content builder
Fixes #9048
-rw-r--r--commands/hugo.go25
-rw-r--r--create/content.go10
-rw-r--r--htesting/test_helpers.go12
-rw-r--r--hugolib/filesystems/basefs.go26
-rw-r--r--hugolib/hugo_sites.go6
-rw-r--r--hugolib/hugo_sites_build.go18
-rw-r--r--hugolib/resource_chain_test.go14
7 files changed, 79 insertions, 32 deletions
diff --git a/commands/hugo.go b/commands/hugo.go
index c19756008..fbe2349a0 100644
--- a/commands/hugo.go
+++ b/commands/hugo.go
@@ -278,7 +278,8 @@ func isTerminal() bool {
return terminal.IsTerminal(os.Stdout)
}
-func (c *commandeer) fullBuild() error {
+func (c *commandeer) fullBuild(noBuildLock bool) error {
+
var (
g errgroup.Group
langCount map[string]uint64
@@ -303,7 +304,7 @@ func (c *commandeer) fullBuild() error {
return nil
}
buildSitesFunc := func() error {
- if err := c.buildSites(); err != nil {
+ if err := c.buildSites(noBuildLock); err != nil {
return errors.Wrap(err, "Error building site")
}
return nil
@@ -496,7 +497,7 @@ func (c *commandeer) build() error {
}
}()
- if err := c.fullBuild(); err != nil {
+ if err := c.fullBuild(false); err != nil {
return err
}
@@ -551,7 +552,7 @@ func (c *commandeer) serverBuild() error {
}
}()
- if err := c.fullBuild(); err != nil {
+ if err := c.fullBuild(false); err != nil {
return err
}
@@ -721,8 +722,8 @@ func (c *commandeer) getDirList() ([]string, error) {
return filenames, nil
}
-func (c *commandeer) buildSites() (err error) {
- return c.hugo().Build(hugolib.BuildCfg{})
+func (c *commandeer) buildSites(noBuildLock bool) (err error) {
+ return c.hugo().Build(hugolib.BuildCfg{NoBuildLock: noBuildLock})
}
func (c *commandeer) handleBuildErr(err error, msg string) {
@@ -750,7 +751,7 @@ func (c *commandeer) rebuildSites(events []fsnotify.Event) error {
visited[home] = true
}
}
- return c.hugo().Build(hugolib.BuildCfg{RecentlyVisited: visited, ErrRecovery: c.wasError}, events...)
+ return c.hugo().Build(hugolib.BuildCfg{NoBuildLock: true, RecentlyVisited: visited, ErrRecovery: c.wasError}, events...)
}
func (c *commandeer) partialReRender(urls ...string) error {
@@ -762,7 +763,7 @@ func (c *commandeer) partialReRender(urls ...string) error {
for _, url := range urls {
visited[url] = true
}
- return c.hugo().Build(hugolib.BuildCfg{RecentlyVisited: visited, PartialReRender: true, ErrRecovery: c.wasError})
+ return c.hugo().Build(hugolib.BuildCfg{NoBuildLock: true, RecentlyVisited: visited, PartialReRender: true, ErrRecovery: c.wasError})
}
func (c *commandeer) fullRebuild(changeType string) {
@@ -809,7 +810,7 @@ func (c *commandeer) fullRebuild(changeType string) {
return
}
- err = c.buildSites()
+ err = c.buildSites(true)
if err != nil {
c.logger.Errorln(err)
} else if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") {
@@ -864,11 +865,17 @@ func (c *commandeer) newWatcher(pollIntervalStr string, dirList ...string) (*wat
for {
select {
case evs := <-watcher.Events:
+ unlock, err := c.hugo().BaseFs.LockBuild()
+ if err != nil {
+ c.logger.Errorln("Failed to acquire a build lock: %s", err)
+ return
+ }
c.handleEvents(watcher, staticSyncer, evs, configSet)
if c.showErrorInBrowser && c.errCount() > 0 {
// Need to reload browser to show the error
livereload.ForceRefresh()
}
+ unlock()
case err := <-watcher.Errors():
if err != nil && !os.IsNotExist(err) {
c.logger.Errorln("Error while watching:", err)
diff --git a/create/content.go b/create/content.go
index 714939f4c..b006e0f2c 100644
--- a/create/content.go
+++ b/create/content.go
@@ -53,6 +53,12 @@ draft: true
// NewContent creates a new content file in h (or a full bundle if the archetype is a directory)
// in targetPath.
func NewContent(h *hugolib.HugoSites, kind, targetPath string) error {
+ unlock, err := h.BaseFs.LockBuild()
+ if err != nil {
+ return fmt.Errorf("failed to acquire a build lock: %s", err)
+ }
+ defer unlock()
+
cf := hugolib.NewContentFactory(h)
if kind == "" {
@@ -138,7 +144,7 @@ func (b *contentBuilder) buildDir() error {
}
- if err := b.h.Build(hugolib.BuildCfg{SkipRender: true, ContentInclusionFilter: contentInclusionFilter}); err != nil {
+ if err := b.h.Build(hugolib.BuildCfg{NoBuildLock: true, SkipRender: true, ContentInclusionFilter: contentInclusionFilter}); err != nil {
return err
}
@@ -200,7 +206,7 @@ func (b *contentBuilder) buildFile() error {
})
}
- if err := b.h.Build(hugolib.BuildCfg{SkipRender: true, ContentInclusionFilter: contentInclusionFilter}); err != nil {
+ if err := b.h.Build(hugolib.BuildCfg{NoBuildLock: true, SkipRender: true, ContentInclusionFilter: contentInclusionFilter}); err != nil {
return err
}
diff --git a/htesting/test_helpers.go b/htesting/test_helpers.go
index 9a1fe86ef..20722f092 100644
--- a/htesting/test_helpers.go
+++ b/htesting/test_helpers.go
@@ -25,6 +25,18 @@ import (
"github.com/spf13/afero"
)
+// IsTest reports whether we're running as a test.
+var IsTest bool
+
+func init() {
+ for _, arg := range os.Args {
+ if strings.HasPrefix(arg, "-test.") {
+ IsTest = true
+ break
+ }
+ }
+}
+
// CreateTempDir creates a temp dir in the given filesystem and
// returns the dirnam and a func that removes it when done.
func CreateTempDir(fs afero.Fs, prefix string) (string, func(), error) {
diff --git a/hugolib/filesystems/basefs.go b/hugolib/filesystems/basefs.go
index dcfee34ff..0a2c31240 100644
--- a/hugolib/filesystems/basefs.go
+++ b/hugolib/filesystems/basefs.go
@@ -24,7 +24,10 @@ import (
"strings"
"sync"
+ "github.com/gohugoio/hugo/htesting"
+
"github.com/gohugoio/hugo/common/loggers"
+ "github.com/rogpeppe/go-internal/lockedfile"
"github.com/gohugoio/hugo/hugofs/files"
@@ -38,6 +41,13 @@ import (
"github.com/spf13/afero"
)
+const (
+ // Used to control concurrency between multiple Hugo instances, e.g.
+ // a running server and building new content with 'hugo new'.
+ // It's placed in the project root.
+ lockFileBuild = ".hugo_build.lock"
+)
+
var filePathSeparator = string(filepath.Separator)
// BaseFs contains the core base filesystems used by Hugo. The name "base" is used
@@ -56,6 +66,21 @@ type BaseFs struct {
PublishFs afero.Fs
theBigFs *filesystemsCollector
+
+ // Locks.
+ buildMu *lockedfile.Mutex // <project>/.hugo_build.lock
+ buildMuTests sync.Mutex // Used in tests.
+}
+
+// Tries to acquire a build lock.
+func (fs *BaseFs) LockBuild() (unlock func(), err error) {
+ if htesting.IsTest {
+ fs.buildMuTests.Lock()
+ return func() {
+ fs.buildMuTests.Unlock()
+ }, nil
+ }
+ return fs.buildMu.Lock()
}
// TODO(bep) we can get regular files in here and that is fine, but
@@ -402,6 +427,7 @@ func NewBase(p *paths.Paths, logger loggers.Logger, options ...func(*BaseFs) err
b := &BaseFs{
SourceFs: sourceFs,
PublishFs: publishFs,
+ buildMu: lockedfile.MutexAt(filepath.Join(p.WorkingDir, lockFileBuild)),
}
for _, opt := range options {
diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go
index 141019a85..91703091b 100644
--- a/hugolib/hugo_sites.go
+++ b/hugolib/hugo_sites.go
@@ -70,9 +70,6 @@ type HugoSites struct {
// If this is running in the dev server.
running bool
- // Serializes rebuilds when server is running.
- runningMu sync.Mutex
-
// Render output formats for all sites.
renderFormats output.Formats
@@ -682,6 +679,9 @@ type BuildCfg struct {
// Can be set to build only with a sub set of the content source.
ContentInclusionFilter *glob.FilenameFilter
+ // Set when the buildlock is already acquired (e.g. the archetype content builder).
+ NoBuildLock bool
+
testCounters *testCounters
}
diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go
index ab3603cc0..6f3955b80 100644
--- a/hugolib/hugo_sites_build.go
+++ b/hugolib/hugo_sites_build.go
@@ -44,19 +44,17 @@ import (
// Build builds all sites. If filesystem events are provided,
// this is considered to be a potential partial rebuild.
func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
- if h.running {
- // Make sure we don't trigger rebuilds in parallel.
- h.runningMu.Lock()
- defer h.runningMu.Unlock()
- } else {
- defer func() {
- h.Close()
- }()
- }
-
ctx, task := trace.NewTask(context.Background(), "Build")
defer task.End()
+ if !config.NoBuildLock {
+ unlock, err := h.BaseFs.LockBuild()
+ if err != nil {
+ return errors.Wrap(err, "failed to acquire a build lock")
+ }
+ defer unlock()
+ }
+
errCollector := h.StartErrorCollector()
errs := make(chan error)
diff --git a/hugolib/resource_chain_test.go b/hugolib/resource_chain_test.go
index 9095f1822..85b1b3abd 100644
--- a/hugolib/resource_chain_test.go
+++ b/hugolib/resource_chain_test.go
@@ -1099,16 +1099,14 @@ class-in-b {
err = build("never", true)
err = herrors.UnwrapErrorWithFileContext(err)
- fe, ok := err.(*herrors.ErrorWithFileContext)
+ _, ok := err.(*herrors.ErrorWithFileContext)
b.Assert(ok, qt.Equals, true)
- if os.Getenv("CI") == "" {
- // TODO(bep) for some reason, we have starting to get
- // execute of template failed: template: index.html:5:25
- // on CI (GitHub action).
- b.Assert(fe.Position().LineNumber, qt.Equals, 5)
- b.Assert(fe.Error(), qt.Contains, filepath.Join(workDir, "assets/css/components/b.css:4:1"))
- }
+ // TODO(bep) for some reason, we have starting to get
+ // execute of template failed: template: index.html:5:25
+ // on CI (GitHub action).
+ //b.Assert(fe.Position().LineNumber, qt.Equals, 5)
+ //b.Assert(fe.Error(), qt.Contains, filepath.Join(workDir, "assets/css/components/b.css:4:1"))
// Remove PostCSS
b.Assert(os.RemoveAll(filepath.Join(workDir, "node_modules")), qt.IsNil)