Skip to content

Commit db28695

Browse files
committed
Fix some server/watch rebuild issues
Two issues: 1. Fixe potential edit-loop in server/watch mode (see below) 2. Drain the cache eviction stack before we start calculating the change set. This should allow more fine grained rebuilds for bigger sites and/or in low memory situations. The fix in 6c68142 wasn't really fixing the complete problem. In Hugo we have some steps that takes more time than others, one example being CSS building with TailwindCSS. The symptom here is that sometimes when you: 1. Edit content or templates that does not trigger a CSS rebuild => Snappy rebuild. 2. Edit stylesheet or add a CSS class to template that triggers a CSS rebuild => relatively slow rebuild (expected) 3. Then back to content editing or template edits that should not trigger a CSS rebuild => relatively slow rebuild (not expected) This commit fixes this by pulling the dynacache GC step up and merge it with the cache buster step. Fixes #13316
1 parent 778f0d9 commit db28695

File tree

6 files changed

+59
-51
lines changed

6 files changed

+59
-51
lines changed

cache/dynacache/dynacache.go

+11-5
Original file line numberDiff line numberDiff line change
@@ -176,11 +176,12 @@ func (c *Cache) ClearMatching(predicatePartition func(k string, p PartitionManag
176176
}
177177

178178
// ClearOnRebuild prepares the cache for a new rebuild taking the given changeset into account.
179-
func (c *Cache) ClearOnRebuild(changeset ...identity.Identity) {
179+
// predicate is optional and will clear any entry for which it returns true.
180+
func (c *Cache) ClearOnRebuild(predicate func(k, v any) bool, changeset ...identity.Identity) {
180181
g := rungroup.Run[PartitionManager](context.Background(), rungroup.Config[PartitionManager]{
181182
NumWorkers: len(c.partitions),
182183
Handle: func(ctx context.Context, partition PartitionManager) error {
183-
partition.clearOnRebuild(changeset...)
184+
partition.clearOnRebuild(predicate, changeset...)
184185
return nil
185186
},
186187
})
@@ -479,7 +480,12 @@ func (p *Partition[K, V]) clearMatching(predicate func(k, v any) bool) {
479480
})
480481
}
481482

482-
func (p *Partition[K, V]) clearOnRebuild(changeset ...identity.Identity) {
483+
func (p *Partition[K, V]) clearOnRebuild(predicate func(k, v any) bool, changeset ...identity.Identity) {
484+
if predicate == nil {
485+
predicate = func(k, v any) bool {
486+
return false
487+
}
488+
}
483489
opts := p.getOptions()
484490
if opts.ClearWhen == ClearNever {
485491
return
@@ -525,7 +531,7 @@ func (p *Partition[K, V]) clearOnRebuild(changeset ...identity.Identity) {
525531
// Second pass needs to be done in a separate loop to catch any
526532
// elements marked as stale in the other partitions.
527533
p.c.DeleteFunc(func(key K, v V) bool {
528-
if shouldDelete(key, v) {
534+
if predicate(key, v) || shouldDelete(key, v) {
529535
p.trace.Log(
530536
logg.StringFunc(
531537
func() string {
@@ -601,7 +607,7 @@ type PartitionManager interface {
601607
adjustMaxSize(addend int) int
602608
getMaxSize() int
603609
getOptions() OptionsPartition
604-
clearOnRebuild(changeset ...identity.Identity)
610+
clearOnRebuild(predicate func(k, v any) bool, changeset ...identity.Identity)
605611
clearMatching(predicate func(k, v any) bool)
606612
clearStale()
607613
}

cache/dynacache/dynacache_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -147,13 +147,13 @@ func TestClear(t *testing.T) {
147147

148148
c.Assert(cache.Keys(predicateAll), qt.HasLen, 4)
149149

150-
cache.ClearOnRebuild()
150+
cache.ClearOnRebuild(nil)
151151

152152
// Stale items are always cleared.
153153
c.Assert(cache.Keys(predicateAll), qt.HasLen, 2)
154154

155155
cache = newTestCache(t)
156-
cache.ClearOnRebuild(identity.StringIdentity("changed"))
156+
cache.ClearOnRebuild(nil, identity.StringIdentity("changed"))
157157

158158
c.Assert(cache.Keys(nil), qt.HasLen, 1)
159159

hugolib/content_map_page.go

+27-26
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,9 @@ func (h *HugoSites) resolveAndClearStateForIdentities(
11231123
l logg.LevelLogger,
11241124
cachebuster func(s string) bool, changes []identity.Identity,
11251125
) error {
1126+
// Drain the cache eviction stack to start fresh.
1127+
h.Deps.MemCache.DrainEvictedIdentities()
1128+
11261129
h.Log.Debug().Log(logg.StringFunc(
11271130
func() string {
11281131
var sb strings.Builder
@@ -1163,17 +1166,32 @@ func (h *HugoSites) resolveAndClearStateForIdentities(
11631166
}
11641167

11651168
// The order matters here:
1166-
// 1. Handle the cache busters first, as those may produce identities for the page reset step.
1169+
// 1. Then GC the cache, which may produce changes.
11671170
// 2. Then reset the page outputs, which may mark some resources as stale.
1168-
// 3. Then GC the cache.
1169-
if cachebuster != nil {
1170-
if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
1171-
ll := l.WithField("substep", "gc dynacache cachebuster")
1172-
h.dynacacheGCCacheBuster(cachebuster)
1173-
return ll, nil
1174-
}); err != nil {
1175-
return err
1171+
if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
1172+
ll := l.WithField("substep", "gc dynacache")
1173+
1174+
predicate := func(k any, v any) bool {
1175+
if cachebuster != nil {
1176+
if s, ok := k.(string); ok {
1177+
return cachebuster(s)
1178+
}
1179+
}
1180+
return false
11761181
}
1182+
1183+
h.MemCache.ClearOnRebuild(predicate, changes...)
1184+
h.Log.Trace(logg.StringFunc(func() string {
1185+
var sb strings.Builder
1186+
sb.WriteString("dynacache keys:\n")
1187+
for _, key := range h.MemCache.Keys(nil) {
1188+
sb.WriteString(fmt.Sprintf(" %s\n", key))
1189+
}
1190+
return sb.String()
1191+
}))
1192+
return ll, nil
1193+
}); err != nil {
1194+
return err
11771195
}
11781196

11791197
// Drain the cache eviction stack.
@@ -1238,23 +1256,6 @@ func (h *HugoSites) resolveAndClearStateForIdentities(
12381256
return err
12391257
}
12401258

1241-
if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
1242-
ll := l.WithField("substep", "gc dynacache")
1243-
1244-
h.MemCache.ClearOnRebuild(changes...)
1245-
h.Log.Trace(logg.StringFunc(func() string {
1246-
var sb strings.Builder
1247-
sb.WriteString("dynacache keys:\n")
1248-
for _, key := range h.MemCache.Keys(nil) {
1249-
sb.WriteString(fmt.Sprintf(" %s\n", key))
1250-
}
1251-
return sb.String()
1252-
}))
1253-
return ll, nil
1254-
}); err != nil {
1255-
return err
1256-
}
1257-
12581259
return nil
12591260
}
12601261

hugolib/hugo_sites_build.go

+12-18
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import (
2727

2828
"github.com/bep/logg"
2929
"github.com/gohugoio/hugo/bufferpool"
30-
"github.com/gohugoio/hugo/cache/dynacache"
3130
"github.com/gohugoio/hugo/deps"
3231
"github.com/gohugoio/hugo/hugofs"
3332
"github.com/gohugoio/hugo/hugofs/files"
@@ -828,6 +827,11 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo
828827
addedContentPaths []*paths.Path
829828
)
830829

830+
var (
831+
addedOrChangedContent []pathChange
832+
changes []identity.Identity
833+
)
834+
831835
for _, ev := range eventInfos {
832836
cpss := h.BaseFs.ResolvePaths(ev.Name)
833837
pss := make([]*paths.Path, len(cpss))
@@ -854,6 +858,13 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo
854858
if err == nil && g != nil {
855859
cacheBusters = append(cacheBusters, g)
856860
}
861+
862+
if ev.added {
863+
changes = append(changes, identity.StructuralChangeAdd)
864+
}
865+
if ev.removed {
866+
changes = append(changes, identity.StructuralChangeRemove)
867+
}
857868
}
858869

859870
if ev.removed {
@@ -865,11 +876,6 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo
865876
}
866877
}
867878

868-
var (
869-
addedOrChangedContent []pathChange
870-
changes []identity.Identity
871-
)
872-
873879
// Find the most specific identity possible.
874880
handleChange := func(pathInfo *paths.Path, delete, isDir bool) {
875881
switch pathInfo.Component() {
@@ -1063,18 +1069,6 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo
10631069

10641070
resourceFiles := h.fileEventsContentPaths(addedOrChangedContent)
10651071

1066-
defer func() {
1067-
// See issue 13316.
1068-
h.MemCache.DrainEvictedIdentitiesMatching(func(ki dynacache.KeyIdentity) bool {
1069-
for _, c := range changes {
1070-
if c.IdentifierBase() == ki.Identity.IdentifierBase() {
1071-
return true
1072-
}
1073-
}
1074-
return false
1075-
})
1076-
}()
1077-
10781072
changed := &WhatChanged{
10791073
needsPagesAssembly: needsPagesAssemble,
10801074
identitySet: make(identity.Identities),

identity/identity.go

+3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ const (
3333

3434
// GenghisKhan is an Identity everyone relates to.
3535
GenghisKhan = StringIdentity("__genghiskhan")
36+
37+
StructuralChangeAdd = StringIdentity("__structural_change_add")
38+
StructuralChangeRemove = StringIdentity("__structural_change_remove")
3639
)
3740

3841
var NopManager = new(nopManager)

resources/resource_factories/bundler/bundler.go

+4
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ func (c *Client) Concat(targetPath string, r resource.Resources) (resource.Resou
9595
}
9696

9797
idm := c.rs.Cfg.NewIdentityManager("concat")
98+
99+
// Re-create on structural changes.
100+
idm.AddIdentity(identity.StructuralChangeAdd, identity.StructuralChangeRemove)
101+
98102
// Add the concatenated resources as dependencies to the composite resource
99103
// so that we can track changes to the individual resources.
100104
idm.AddIdentityForEach(identity.ForEeachIdentityProviderFunc(

0 commit comments

Comments
 (0)