From 2216028620fd68d5d8fc18511096eeb5216ed2ba Mon Sep 17 00:00:00 2001 From: n1xx1 Date: Thu, 7 Aug 2025 11:17:14 +0200 Subject: [PATCH] Add a key to the partialCached deadlock prevention Fixes #13889 --- tpl/partials/partials.go | 14 ++++++------ tpl/partials/partials_integration_test.go | 26 +++++++++++++++++++++++ tpl/template.go | 1 + tpl/tplimpl/templatestore.go | 5 +++++ 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/tpl/partials/partials.go b/tpl/partials/partials.go index 57a2aa280..63daadb42 100644 --- a/tpl/partials/partials.go +++ b/tpl/partials/partials.go @@ -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 } diff --git a/tpl/partials/partials_integration_test.go b/tpl/partials/partials_integration_test.go index 0fa47104d..a77b025ed 100644 --- a/tpl/partials/partials_integration_test.go +++ b/tpl/partials/partials_integration_test.go @@ -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() diff --git a/tpl/template.go b/tpl/template.go index 877422123..fde8b4bcc 100644 --- a/tpl/template.go +++ b/tpl/template.go @@ -161,6 +161,7 @@ type CurrentTemplateInfoCommonOps interface { type CurrentTemplateInfo struct { Parent *CurrentTemplateInfo Level int + Key string CurrentTemplateInfoOps } diff --git a/tpl/tplimpl/templatestore.go b/tpl/tplimpl/templatestore.go index caf53c005..0c03b2cc1 100644 --- a/tpl/tplimpl/templatestore.go +++ b/tpl/tplimpl/templatestore.go @@ -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, }