summaryrefslogtreecommitdiffstats
path: root/tpl/partials
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2022-02-17 16:51:19 +0100
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2022-02-17 18:47:36 +0100
commit929808190fb0649dcfa6def39cd1afe1d380087c (patch)
treea4405f12f93815351793da666b7ec0dc2d97a7a7 /tpl/partials
parent667f3a4ba880b14f346161891cd43e2ba9ce9b9d (diff)
tpl/partials: Fix recently introduced deadlock in partials cache
The change in lock logic for `partialCached` in 0927cf739fee9646c7fb917965799d9acf080922 was naive as it didn't consider cached partials calling other cached partials. This changeset may look on the large side for this particular issue, but it pulls in part of a working branch, introducing `context.Context` in the template execution. Note that the context is only partially implemented in this PR, but the upcoming use cases will, as one example, include having access to the top "dot" (e.g. `Page`) all the way down into partials and shortcodes etc. The earlier benchmarks rerun against master: ```bash name old time/op new time/op delta IncludeCached-10 13.6ms ± 2% 13.8ms ± 1% ~ (p=0.343 n=4+4) name old alloc/op new alloc/op delta IncludeCached-10 5.30MB ± 0% 5.35MB ± 0% +0.96% (p=0.029 n=4+4) name old allocs/op new allocs/op delta IncludeCached-10 74.7k ± 0% 75.3k ± 0% +0.77% (p=0.029 n=4+4) ``` Fixes #9519
Diffstat (limited to 'tpl/partials')
-rw-r--r--tpl/partials/integration_test.go28
-rw-r--r--tpl/partials/partials.go53
2 files changed, 61 insertions, 20 deletions
diff --git a/tpl/partials/integration_test.go b/tpl/partials/integration_test.go
index 5b6c18598..446e47118 100644
--- a/tpl/partials/integration_test.go
+++ b/tpl/partials/integration_test.go
@@ -75,6 +75,34 @@ partialCached: foo
`)
}
+// Issue 9519
+func TestIncludeCachedRecursion(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- config.toml --
+baseURL = 'http://example.com/'
+-- layouts/index.html --
+{{ partials.IncludeCached "p1.html" . }}
+-- layouts/partials/p1.html --
+{{ partials.IncludeCached "p2.html" . }}
+-- layouts/partials/p2.html --
+P2
+
+ `
+
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{
+ T: t,
+ TxtarString: files,
+ },
+ ).Build()
+
+ b.AssertFileContent("public/index.html", `
+P2
+`)
+}
+
func TestIncludeCacheHints(t *testing.T) {
t.Parallel()
diff --git a/tpl/partials/partials.go b/tpl/partials/partials.go
index 787b49ed3..500f5d1a3 100644
--- a/tpl/partials/partials.go
+++ b/tpl/partials/partials.go
@@ -16,6 +16,7 @@
package partials
import (
+ "context"
"errors"
"fmt"
"html/template"
@@ -100,8 +101,9 @@ func (c *contextWrapper) Set(in interface{}) string {
// If the partial contains a return statement, that value will be returned.
// Else, the rendered output will be returned:
// A string if the partial is a text/template, or template.HTML when html/template.
-func (ns *Namespace) Include(name string, contextList ...interface{}) (interface{}, error) {
- name, result, err := ns.include(name, contextList...)
+// Note that ctx is provided by Hugo, not the end user.
+func (ns *Namespace) Include(ctx context.Context, name string, contextList ...interface{}) (interface{}, error) {
+ name, result, err := ns.include(ctx, name, contextList...)
if err != nil {
return result, err
}
@@ -115,10 +117,10 @@ func (ns *Namespace) Include(name string, contextList ...interface{}) (interface
// include is a helper function that lookups and executes the named partial.
// Returns the final template name and the rendered output.
-func (ns *Namespace) include(name string, contextList ...interface{}) (string, interface{}, error) {
- var context interface{}
- if len(contextList) > 0 {
- context = contextList[0]
+func (ns *Namespace) include(ctx context.Context, name string, dataList ...interface{}) (string, interface{}, error) {
+ var data interface{}
+ if len(dataList) > 0 {
+ data = dataList[0]
}
var n string
@@ -149,8 +151,8 @@ func (ns *Namespace) include(name string, contextList ...interface{}) (string, i
// Wrap the context sent to the template to capture the return value.
// Note that the template is rewritten to make sure that the dot (".")
// and the $ variable points to Arg.
- context = &contextWrapper{
- Arg: context,
+ data = &contextWrapper{
+ Arg: data,
}
// We don't care about any template output.
@@ -161,13 +163,13 @@ func (ns *Namespace) include(name string, contextList ...interface{}) (string, i
w = b
}
- if err := ns.deps.Tmpl().Execute(templ, w, context); err != nil {
- return "", "", err
+ if err := ns.deps.Tmpl().ExecuteWithContext(ctx, templ, w, data); err != nil {
+ return "", nil, err
}
var result interface{}
- if ctx, ok := context.(*contextWrapper); ok {
+ if ctx, ok := data.(*contextWrapper); ok {
result = ctx.Result
} else if _, ok := templ.(*texttemplate.Template); ok {
result = w.(fmt.Stringer).String()
@@ -179,17 +181,18 @@ func (ns *Namespace) include(name string, contextList ...interface{}) (string, i
}
// IncludeCached executes and caches partial templates. The cache is created with name+variants as the key.
-func (ns *Namespace) IncludeCached(name string, context interface{}, variants ...interface{}) (interface{}, error) {
+// Note that ctx is provided by Hugo, not the end user.
+func (ns *Namespace) IncludeCached(ctx context.Context, name string, context interface{}, variants ...interface{}) (interface{}, error) {
key, err := createKey(name, variants...)
if err != nil {
return nil, err
}
- result, err := ns.getOrCreate(key, context)
+ result, err := ns.getOrCreate(ctx, key, context)
if err == errUnHashable {
// Try one more
key.variant = helpers.HashString(key.variant)
- result, err = ns.getOrCreate(key, context)
+ result, err = ns.getOrCreate(ctx, key, context)
}
return result, err
@@ -218,7 +221,7 @@ func createKey(name string, variants ...interface{}) (partialCacheKey, error) {
var errUnHashable = errors.New("unhashable")
-func (ns *Namespace) getOrCreate(key partialCacheKey, context interface{}) (result interface{}, err error) {
+func (ns *Namespace) getOrCreate(ctx context.Context, key partialCacheKey, context interface{}) (result interface{}, err error) {
start := time.Now()
defer func() {
if r := recover(); r != nil {
@@ -230,9 +233,16 @@ func (ns *Namespace) getOrCreate(key partialCacheKey, context interface{}) (resu
}
}()
- ns.cachedPartials.RLock()
+ // We may already have a write lock.
+ hasLock := tpl.GetHasLockFromContext(ctx)
+
+ if !hasLock {
+ ns.cachedPartials.RLock()
+ }
p, ok := ns.cachedPartials.p[key]
- ns.cachedPartials.RUnlock()
+ if !hasLock {
+ ns.cachedPartials.RUnlock()
+ }
if ok {
if ns.deps.Metrics != nil {
@@ -246,11 +256,14 @@ func (ns *Namespace) getOrCreate(key partialCacheKey, context interface{}) (resu
return p, nil
}
- ns.cachedPartials.Lock()
- defer ns.cachedPartials.Unlock()
+ if !hasLock {
+ ns.cachedPartials.Lock()
+ defer ns.cachedPartials.Unlock()
+ ctx = tpl.SetHasLockInContext(ctx, true)
+ }
var name string
- name, p, err = ns.include(key.name, context)
+ name, p, err = ns.include(ctx, key.name, context)
if err != nil {
return nil, err
}