diff options
author | Bjørn Erik Pedersen <[email protected]> | 2024-06-08 11:52:22 +0200 |
---|---|---|
committer | Bjørn Erik Pedersen <[email protected]> | 2024-06-23 11:25:47 +0200 |
commit | 6cd0784e447f18e009cbbf30de471e486f7cf356 (patch) | |
tree | 0684d05d7e487ebe93463636ed8b5a1bb78e4704 /hugolib | |
parent | 8731d8822216dd3c7587769e3cf5d98690717b0c (diff) | |
download | hugo-6cd0784e447f18e009cbbf30de471e486f7cf356.tar.gz hugo-6cd0784e447f18e009cbbf30de471e486f7cf356.zip |
Implement defer
Closes #8086
Closes #12589
Diffstat (limited to 'hugolib')
-rw-r--r-- | hugolib/content_map_page.go | 77 | ||||
-rw-r--r-- | hugolib/filesystems/basefs.go | 2 | ||||
-rw-r--r-- | hugolib/hugo_sites_build.go | 204 | ||||
-rw-r--r-- | hugolib/hugo_sites_build_errors_test.go | 17 | ||||
-rw-r--r-- | hugolib/site.go | 10 | ||||
-rw-r--r-- | hugolib/site_new.go | 18 | ||||
-rw-r--r-- | hugolib/site_render.go | 4 | ||||
-rw-r--r-- | hugolib/site_sections.go | 4 |
8 files changed, 260 insertions, 76 deletions
diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go index f9709df15..0a9063e23 100644 --- a/hugolib/content_map_page.go +++ b/hugolib/content_map_page.go @@ -33,6 +33,7 @@ import ( "github.com/gohugoio/hugo/common/rungroup" "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/hugofs/files" + "github.com/gohugoio/hugo/hugofs/glob" "github.com/gohugoio/hugo/hugolib/doctree" "github.com/gohugoio/hugo/hugolib/pagesfromdata" "github.com/gohugoio/hugo/identity" @@ -1002,7 +1003,7 @@ func (m *pageMap) debugPrint(prefix string, maxLevel int, w io.Writer) { } const indentStr = " " p := n.(*pageState) - s := strings.TrimPrefix(keyPage, paths.CommonDir(prevKey, keyPage)) + s := strings.TrimPrefix(keyPage, paths.CommonDirPath(prevKey, keyPage)) lenIndent := len(keyPage) - len(s) fmt.Fprint(w, strings.Repeat(indentStr, lenIndent)) info := fmt.Sprintf("%s lm: %s (%s)", s, p.Lastmod().Format("2006-01-02"), p.Kind()) @@ -1047,6 +1048,59 @@ func (m *pageMap) debugPrint(prefix string, maxLevel int, w io.Writer) { } } +func (h *HugoSites) dynacacheGCFilenameIfNotWatchedAndDrainMatching(filename string) { + cpss := h.BaseFs.ResolvePaths(filename) + if len(cpss) == 0 { + return + } + // Compile cache busters. + var cacheBusters []func(string) bool + for _, cps := range cpss { + if cps.Watch { + continue + } + np := glob.NormalizePath(path.Join(cps.Component, cps.Path)) + g, err := h.ResourceSpec.BuildConfig().MatchCacheBuster(h.Log, np) + if err == nil && g != nil { + cacheBusters = append(cacheBusters, g) + } + } + if len(cacheBusters) == 0 { + return + } + cacheBusterOr := func(s string) bool { + for _, cb := range cacheBusters { + if cb(s) { + return true + } + } + return false + } + + h.dynacacheGCCacheBuster(cacheBusterOr) + + // We want to avoid that evicted items in the above is considered in the next step server change. + _ = h.MemCache.DrainEvictedIdentitiesMatching(func(ki dynacache.KeyIdentity) bool { + return cacheBusterOr(ki.Key.(string)) + }) +} + +func (h *HugoSites) dynacacheGCCacheBuster(cachebuster func(s string) bool) { + if cachebuster == nil { + return + } + shouldDelete := func(k, v any) bool { + var b bool + if s, ok := k.(string); ok { + b = cachebuster(s) + } + + return b + } + + h.MemCache.ClearMatching(nil, shouldDelete) +} + func (h *HugoSites) resolveAndClearStateForIdentities( ctx context.Context, l logg.LevelLogger, @@ -1095,25 +1149,10 @@ func (h *HugoSites) resolveAndClearStateForIdentities( // 1. Handle the cache busters first, as those may produce identities for the page reset step. // 2. Then reset the page outputs, which may mark some resources as stale. // 3. Then GC the cache. - // TOOD1 if cachebuster != nil { if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) { ll := l.WithField("substep", "gc dynacache cachebuster") - - shouldDelete := func(k, v any) bool { - if cachebuster == nil { - return false - } - var b bool - if s, ok := k.(string); ok { - b = cachebuster(s) - } - - return b - } - - h.MemCache.ClearMatching(nil, shouldDelete) - + h.dynacacheGCCacheBuster(cachebuster) return ll, nil }); err != nil { return err @@ -1123,7 +1162,9 @@ func (h *HugoSites) resolveAndClearStateForIdentities( // Drain the cache eviction stack. evicted := h.Deps.MemCache.DrainEvictedIdentities() if len(evicted) < 200 { - changes = append(changes, evicted...) + for _, c := range evicted { + changes = append(changes, c.Identity) + } } else { // Mass eviction, we might as well invalidate everything. changes = []identity.Identity{identity.GenghisKhan} diff --git a/hugolib/filesystems/basefs.go b/hugolib/filesystems/basefs.go index b3e3284d5..cb7846cd1 100644 --- a/hugolib/filesystems/basefs.go +++ b/hugolib/filesystems/basefs.go @@ -720,7 +720,7 @@ func (b *sourceFilesystemsBuilder) createOverlayFs( ModuleOrdinal: md.ordinal, IsProject: md.isMainProject, Meta: &hugofs.FileMeta{ - Watch: md.Watch(), + Watch: !mount.DisableWatch && md.Watch(), Weight: mountWeight, InclusionFilter: inclusionFilter, }, diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go index 12eb6a5f8..65ce946e9 100644 --- a/hugolib/hugo_sites_build.go +++ b/hugolib/hugo_sites_build.go @@ -26,6 +26,7 @@ import ( "time" "github.com/bep/logg" + "github.com/gohugoio/hugo/bufferpool" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugofs/files" @@ -173,6 +174,16 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error { h.SendError(fmt.Errorf("postRenderOnce: %w", err)) } + // Make sure to write any build stats to disk first so it's available + // to the post processors. + if err := h.writeBuildStats(); err != nil { + return err + } + + if err := h.renderDeferred(infol); err != nil { + h.SendError(fmt.Errorf("renderDeferred: %w", err)) + } + if err := h.postProcess(infol); err != nil { h.SendError(fmt.Errorf("postProcess: %w", err)) } @@ -352,47 +363,172 @@ func (h *HugoSites) render(l logg.LevelLogger, config *BuildCfg) error { continue } - siteRenderContext.outIdx = siteOutIdx - siteRenderContext.sitesOutIdx = i - i++ - - select { - case <-h.Done(): - return nil - default: - for _, s2 := range h.Sites { - // We render site by site, but since the content is lazily rendered - // and a site can "borrow" content from other sites, every site - // needs this set. - s2.rc = &siteRenderingContext{Format: renderFormat} - - if err := s2.preparePagesForRender(s == s2, siteRenderContext.sitesOutIdx); err != nil { - return err + if err := func() error { + rc := tpl.RenderingContext{Site: s, SiteOutIdx: siteOutIdx} + h.BuildState.StartStageRender(rc) + defer h.BuildState.StopStageRender(rc) + + siteRenderContext.outIdx = siteOutIdx + siteRenderContext.sitesOutIdx = i + i++ + + select { + case <-h.Done(): + return nil + default: + for _, s2 := range h.Sites { + if err := s2.preparePagesForRender(s == s2, siteRenderContext.sitesOutIdx); err != nil { + return err + } + } + if !config.SkipRender { + ll := l.WithField("substep", "pages"). + WithField("site", s.language.Lang). + WithField("outputFormat", renderFormat.Name) + + start := time.Now() + + if config.PartialReRender { + if err := s.renderPages(siteRenderContext); err != nil { + return err + } + } else { + if err := s.render(siteRenderContext); err != nil { + return err + } + } + loggers.TimeTrackf(ll, start, nil, "") } } - if !config.SkipRender { - ll := l.WithField("substep", "pages"). - WithField("site", s.language.Lang). - WithField("outputFormat", renderFormat.Name) + return nil + }(); err != nil { + return err + } - start := time.Now() + } + } - if config.PartialReRender { - if err := s.renderPages(siteRenderContext); err != nil { - return err - } - } else { - if err := s.render(siteRenderContext); err != nil { + return nil +} + +func (h *HugoSites) renderDeferred(l logg.LevelLogger) error { + l = l.WithField("step", "render deferred") + start := time.Now() + + var deferredCount int + + for rc, de := range h.Deps.BuildState.DeferredExecutionsGroupedByRenderingContext { + if de.FilenamesWithPostPrefix.Len() == 0 { + continue + } + + deferredCount += de.FilenamesWithPostPrefix.Len() + + s := rc.Site.(*Site) + for _, s2 := range h.Sites { + if err := s2.preparePagesForRender(s == s2, rc.SiteOutIdx); err != nil { + return err + } + } + if err := s.executeDeferredTemplates(de); err != nil { + return herrors.ImproveRenderErr(err) + } + } + + loggers.TimeTrackf(l, start, logg.Fields{ + logg.Field{Name: "count", Value: deferredCount}, + }, "") + + return nil +} + +func (s *Site) executeDeferredTemplates(de *deps.DeferredExecutions) error { + handleFile := func(filename string) error { + content, err := afero.ReadFile(s.BaseFs.PublishFs, filename) + if err != nil { + return err + } + + k := 0 + changed := false + + for { + if k >= len(content) { + break + } + l := bytes.Index(content[k:], []byte(tpl.HugoDeferredTemplatePrefix)) + if l == -1 { + break + } + m := bytes.Index(content[k+l:], []byte(tpl.HugoDeferredTemplateSuffix)) + len(tpl.HugoDeferredTemplateSuffix) + + low, high := k+l, k+l+m + + forward := l + m + id := string(content[low:high]) + + if err := func() error { + deferred, found := de.Executions.Get(id) + if !found { + panic(fmt.Sprintf("deferred execution with id %q not found", id)) + } + deferred.Mu.Lock() + defer deferred.Mu.Unlock() + + if !deferred.Executed { + tmpl := s.Deps.Tmpl() + templ, found := tmpl.Lookup(deferred.TemplateName) + if !found { + panic(fmt.Sprintf("template %q not found", deferred.TemplateName)) + } + + if err := func() error { + buf := bufferpool.GetBuffer() + defer bufferpool.PutBuffer(buf) + + err = tmpl.ExecuteWithContext(deferred.Ctx, templ, buf, deferred.Data) + if err != nil { return err } + deferred.Result = buf.String() + deferred.Executed = true + + return nil + }(); err != nil { + return err } - loggers.TimeTrackf(ll, start, nil, "") } + + content = append(content[:low], append([]byte(deferred.Result), content[high:]...)...) + changed = true + + return nil + }(); err != nil { + return err } + + k += forward } + + if changed { + return afero.WriteFile(s.BaseFs.PublishFs, filename, content, 0o666) + } + + return nil } - return nil + g := rungroup.Run[string](context.Background(), rungroup.Config[string]{ + NumWorkers: s.h.numWorkers, + Handle: func(ctx context.Context, filename string) error { + return handleFile(filename) + }, + }) + + de.FilenamesWithPostPrefix.ForEeach(func(filename string, _ bool) { + g.Enqueue(filename) + }) + + return g.Wait() } // / postRenderOnce runs some post processing that only needs to be done once, e.g. printing of unused templates. @@ -428,12 +564,6 @@ func (h *HugoSites) postProcess(l logg.LevelLogger) error { l = l.WithField("step", "postProcess") defer loggers.TimeTrackf(l, time.Now(), nil, "") - // Make sure to write any build stats to disk first so it's available - // to the post processors. - if err := h.writeBuildStats(); err != nil { - return err - } - // This will only be set when js.Build have been triggered with // imports that resolves to the project or a module. // Write a jsconfig.json file to the project's /asset directory @@ -600,6 +730,10 @@ func (h *HugoSites) writeBuildStats() error { } } + // This step may be followed by a post process step that may + // rebuild e.g. CSS, so clear any cache that's defined for the hugo_stats.json. + h.dynacacheGCFilenameIfNotWatchedAndDrainMatching(filename) + return nil } diff --git a/hugolib/hugo_sites_build_errors_test.go b/hugolib/hugo_sites_build_errors_test.go index 5a8b9f76f..71afe6767 100644 --- a/hugolib/hugo_sites_build_errors_test.go +++ b/hugolib/hugo_sites_build_errors_test.go @@ -628,3 +628,20 @@ title: "A page" b.CreateSites().BuildFail(BuildCfg{}) } + +func TestErrorTemplateRuntime(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +-- layouts/index.html -- +Home. +{{ .ThisDoesNotExist }} + ` + + b, err := TestE(t, files) + + b.Assert(err, qt.Not(qt.IsNil)) + b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`/layouts/index.html:2:3`)) + b.Assert(err.Error(), qt.Contains, `can't evaluate field ThisDoesNotExist`) +} diff --git a/hugolib/site.go b/hugolib/site.go index b4b89975d..2113c4f20 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -62,7 +62,7 @@ import ( ) func (s *Site) Taxonomies() page.TaxonomyList { - s.checkReady() + s.CheckReady() s.init.taxonomies.Do(context.Background()) return s.taxonomies } @@ -200,12 +200,8 @@ func (s *Site) prepareInits() { }) } -type siteRenderingContext struct { - output.Format -} - func (s *Site) Menus() navigation.Menus { - s.checkReady() + s.CheckReady() s.init.menus.Do(context.Background()) return s.menus } @@ -810,7 +806,7 @@ func (s *Site) errorCollator(results <-chan error, errs chan<- error) { // as possible for existing sites. Most sites will use {{ .Site.GetPage "section" "my/section" }}, // i.e. 2 arguments, so we test for that. func (s *Site) GetPage(ref ...string) (page.Page, error) { - s.checkReady() + s.CheckReady() p, err := s.s.getPageForRefs(ref...) if p == nil { diff --git a/hugolib/site_new.go b/hugolib/site_new.go index 2ba5ef2fb..cb6630cb3 100644 --- a/hugolib/site_new.go +++ b/hugolib/site_new.go @@ -88,10 +88,6 @@ type Site struct { publisher publisher.Publisher frontmatterHandler pagemeta.FrontMatterHandler - // We render each site for all the relevant output formats in serial with - // this rendering context pointing to the current one. - rc *siteRenderingContext - // The output formats that we need to render this site in. This slice // will be fixed once set. // This will be the union of Site.Pages' outputFormats. @@ -439,7 +435,7 @@ func (s *Site) Current() page.Site { // MainSections returns the list of main sections. func (s *Site) MainSections() []string { - s.checkReady() + s.CheckReady() return s.conf.C.MainSections } @@ -458,7 +454,7 @@ func (s *Site) BaseURL() string { // Deprecated: Use .Site.Lastmod instead. func (s *Site) LastChange() time.Time { - s.checkReady() + s.CheckReady() hugo.Deprecate(".Site.LastChange", "Use .Site.Lastmod instead.", "v0.123.0") return s.lastmod } @@ -547,7 +543,7 @@ func (s *Site) ForEeachIdentityByName(name string, f func(identity.Identity) boo // Pages returns all pages. // This is for the current language only. func (s *Site) Pages() page.Pages { - s.checkReady() + s.CheckReady() return s.pageMap.getPagesInSection( pageMapQueryPagesInSection{ pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{ @@ -564,7 +560,7 @@ func (s *Site) Pages() page.Pages { // RegularPages returns all the regular pages. // This is for the current language only. func (s *Site) RegularPages() page.Pages { - s.checkReady() + s.CheckReady() return s.pageMap.getPagesInSection( pageMapQueryPagesInSection{ pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{ @@ -579,17 +575,17 @@ func (s *Site) RegularPages() page.Pages { // AllPages returns all pages for all sites. func (s *Site) AllPages() page.Pages { - s.checkReady() + s.CheckReady() return s.h.Pages() } // AllRegularPages returns all regular pages for all sites. func (s *Site) AllRegularPages() page.Pages { - s.checkReady() + s.CheckReady() return s.h.RegularPages() } -func (s *Site) checkReady() { +func (s *Site) CheckReady() { if s.state != siteStateReady { panic("this method cannot be called before the site is fully initialized") } diff --git a/hugolib/site_render.go b/hugolib/site_render.go index a7ecf89af..83f2fce89 100644 --- a/hugolib/site_render.go +++ b/hugolib/site_render.go @@ -111,7 +111,7 @@ func (s *Site) renderPages(ctx *siteRenderContext) error { err := <-errs if err != nil { - return fmt.Errorf("failed to render pages: %w", herrors.ImproveIfNilPointer(err)) + return fmt.Errorf("failed to render pages: %w", herrors.ImproveRenderErr(err)) } return nil } @@ -226,7 +226,7 @@ func (s *Site) renderPaginator(p *pageState, templ tpl.Template) error { paginatePath := s.Conf.Pagination().Path d := p.targetPathDescriptor - f := p.s.rc.Format + f := p.outputFormat() d.Type = f if p.paginator.current == nil || p.paginator.current != p.paginator.current.First() { diff --git a/hugolib/site_sections.go b/hugolib/site_sections.go index 03d662b9f..385f3f291 100644 --- a/hugolib/site_sections.go +++ b/hugolib/site_sections.go @@ -19,12 +19,12 @@ import ( // Sections returns the top level sections. func (s *Site) Sections() page.Pages { - s.checkReady() + s.CheckReady() return s.Home().Sections() } // Home is a shortcut to the home page, equivalent to .Site.GetPage "home". func (s *Site) Home() page.Page { - s.checkReady() + s.CheckReady() return s.s.home } |