]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
Add a key to the partialCached deadlock prevention
authorn1xx1 <me@simonemiraglia.it>
Thu, 7 Aug 2025 09:17:14 +0000 (11:17 +0200)
committerGitHub <noreply@github.com>
Thu, 7 Aug 2025 09:17:14 +0000 (11:17 +0200)
Fixes #13889

tpl/partials/partials.go
tpl/partials/partials_integration_test.go
tpl/template.go
tpl/tplimpl/templatestore.go

index 57a2aa2801f02e1358749878354a186a67263034..63daadb42508896a37f20ad17e475524e6abc8c6 100644 (file)
@@ -126,7 +126,7 @@ func (ns *Namespace) include(ctx context.Context, name string, dataList ...any)
        if err != nil {
                return includeResult{err: err}
        }
-       return ns.doInclude(ctx, v, dataList...)
+       return ns.doInclude(ctx, "", v, dataList...)
 }
 
 func (ns *Namespace) lookup(name string) (*tplimpl.TemplInfo, error) {
@@ -144,7 +144,7 @@ func (ns *Namespace) lookup(name string) (*tplimpl.TemplInfo, error) {
 
 // include is a helper function that lookups and executes the named partial.
 // Returns the final template name and the rendered output.
-func (ns *Namespace) doInclude(ctx context.Context, templ *tplimpl.TemplInfo, dataList ...any) includeResult {
+func (ns *Namespace) doInclude(ctx context.Context, key string, templ *tplimpl.TemplInfo, dataList ...any) includeResult {
        var data any
        if len(dataList) > 0 {
                data = dataList[0]
@@ -170,7 +170,7 @@ func (ns *Namespace) doInclude(ctx context.Context, templ *tplimpl.TemplInfo, da
                w = b
        }
 
-       if err := ns.deps.GetTemplateStore().ExecuteWithContext(ctx, templ, w, data); err != nil {
+       if err := ns.deps.GetTemplateStore().ExecuteWithContextAndKey(ctx, key, templ, w, data); err != nil {
                return includeResult{err: err}
        }
 
@@ -198,6 +198,8 @@ func (ns *Namespace) IncludeCached(ctx context.Context, name string, context any
                Name:     name,
                Variants: variants,
        }
+       keyString := key.Key()
+
        depsManagerIn := tpl.Context.GetDependencyManagerInCurrentScope(ctx)
        ti, err := ns.lookup(name)
        if err != nil {
@@ -206,7 +208,7 @@ func (ns *Namespace) IncludeCached(ctx context.Context, name string, context any
 
        if parent := tpl.Context.CurrentTemplate.Get(ctx); parent != nil {
                for parent != nil {
-                       if parent.CurrentTemplateInfoOps == ti {
+                       if parent.CurrentTemplateInfoOps == ti && parent.Key == keyString {
                                // This will deadlock if we continue.
                                return nil, fmt.Errorf("circular call stack detected in partial %q", ti.Filename())
                        }
@@ -214,7 +216,7 @@ func (ns *Namespace) IncludeCached(ctx context.Context, name string, context any
                }
        }
 
-       r, found, err := ns.cachedPartials.cache.GetOrCreate(key.Key(), func(string) (includeResult, error) {
+       r, found, err := ns.cachedPartials.cache.GetOrCreate(keyString, func(string) (includeResult, error) {
                var depsManagerShared identity.Manager
                if ns.deps.Conf.Watching() {
                        // We need to create a shared dependency manager to pass downwards
@@ -222,7 +224,7 @@ func (ns *Namespace) IncludeCached(ctx context.Context, name string, context any
                        depsManagerShared = identity.NewManager("partials")
                        ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, depsManagerShared.(identity.DependencyManagerScopedProvider))
                }
-               r := ns.doInclude(ctx, ti, context)
+               r := ns.doInclude(ctx, keyString, ti, context)
                if ns.deps.Conf.Watching() {
                        r.mangager = depsManagerShared
                }
index 0fa47104d5211a3c915e03620ea1a8469fb8709a..a77b025ed8017a535d59e9a5d86d251e5b864de5 100644 (file)
@@ -299,6 +299,32 @@ timeout = '200ms'
        b.Assert(err.Error(), qt.Contains, `error calling partialCached: circular call stack detected in partial`)
 }
 
+// See Issue #13889
+func TestIncludeCachedDifferentKey(t *testing.T) {
+       t.Parallel()
+
+       files := `
+-- config.toml --
+baseURL = 'http://example.com/'
+timeout = '200ms'
+-- layouts/index.html --
+{{ partialCached "foo.html" "a" "a" }}
+-- layouts/partials/foo.html --
+{{ if eq . "a" }}
+{{ partialCached "bar.html" . }}
+{{ else }}
+DONE
+{{ end }}
+-- layouts/partials/bar.html --
+{{ partialCached "foo.html" "b" "b" }}
+  `
+       b := hugolib.Test(t, files)
+
+       b.AssertFileContent("public/index.html", `
+DONE
+`)
+}
+
 // See Issue #10789
 func TestReturnExecuteFromTemplateInPartial(t *testing.T) {
        t.Parallel()
index 877422123f3c2a2562c9c8fa81eadcf7d7747b64..fde8b4bcc8524957687d25a58e7640f7ad295199 100644 (file)
@@ -161,6 +161,7 @@ type CurrentTemplateInfoCommonOps interface {
 type CurrentTemplateInfo struct {
        Parent *CurrentTemplateInfo
        Level  int
+       Key    string
        CurrentTemplateInfoOps
 }
 
index caf53c0055b0665175a2c81e0060040b80921b3d..0c03b2cc1866152c2d9cd36f19b237dbb1c0398b 100644 (file)
@@ -485,6 +485,10 @@ func (s *TemplateStore) FindAllBaseTemplateCandidates(overlayKey string, desc Te
 }
 
 func (t *TemplateStore) ExecuteWithContext(ctx context.Context, ti *TemplInfo, wr io.Writer, data any) error {
+       return t.ExecuteWithContextAndKey(ctx, "", ti, wr, data)
+}
+
+func (t *TemplateStore) ExecuteWithContextAndKey(ctx context.Context, key string, ti *TemplInfo, wr io.Writer, data any) error {
        defer func() {
                ti.executionCounter.Add(1)
                if ti.base != nil {
@@ -502,6 +506,7 @@ func (t *TemplateStore) ExecuteWithContext(ctx context.Context, ti *TemplInfo, w
        currentTi := &tpl.CurrentTemplateInfo{
                Parent:                 parent,
                Level:                  level,
+               Key:                    key,
                CurrentTemplateInfoOps: ti,
        }