tpl/partials: Fix recently introduced deadlock in partials cache

The change in lock logic for `partialCached` in  0927cf739f 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
This commit is contained in:
Bjørn Erik Pedersen
2022-02-17 16:51:19 +01:00
parent 667f3a4ba8
commit 929808190f
9 changed files with 207 additions and 58 deletions

View File

@@ -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
}