Skip to content

Commit

Permalink
Fix "context canceled" with partial
Browse files Browse the repository at this point in the history
Make sure the context used for timeouts isn't created based on the incoming
context, as we have cases where this can cancel the context prematurely.

Fixes #10789
  • Loading branch information
bep committed Mar 4, 2023
1 parent 184a67a commit 3bbeb56
Show file tree
Hide file tree
Showing 5 changed files with 40 additions and 14 deletions.
7 changes: 4 additions & 3 deletions lazy/init.go
Expand Up @@ -180,22 +180,23 @@ func (ini *Init) checkDone() {
}

func (ini *Init) withTimeout(ctx context.Context, timeout time.Duration, f func(ctx context.Context) (any, error)) (any, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
// Create a new context with a timeout not connected to the incoming context.
waitCtx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
c := make(chan verr, 1)

go func() {
v, err := f(ctx)
select {
case <-ctx.Done():
case <-waitCtx.Done():
return
default:
c <- verr{v: v, err: err}
}
}()

select {
case <-ctx.Done():
case <-waitCtx.Done():
return nil, errors.New("timed out initializing value. You may have a circular loop in a shortcode, or your site may have resources that take longer to build than the `timeout` limit in your Hugo config file.")
case ve := <-c:
return ve.v, ve.err
Expand Down
6 changes: 0 additions & 6 deletions lazy/init_test.go
Expand Up @@ -126,12 +126,6 @@ func TestInitAddWithTimeoutTimeout(t *testing.T) {

init := New().AddWithTimeout(100*time.Millisecond, func(ctx context.Context) (any, error) {
time.Sleep(500 * time.Millisecond)
select {
case <-ctx.Done():
return nil, nil
default:
}
t.Fatal("slept")
return nil, nil
})

Expand Down
4 changes: 2 additions & 2 deletions resources/transform.go
Expand Up @@ -164,12 +164,12 @@ type resourceAdapter struct {
*resourceAdapterInner
}

func (r *resourceAdapter) Content(context.Context) (any, error) {
func (r *resourceAdapter) Content(ctx context.Context) (any, error) {
r.init(false, true)
if r.transformationsErr != nil {
return nil, r.transformationsErr
}
return r.target.Content(context.Background())
return r.target.Content(ctx)
}

func (r *resourceAdapter) Err() resource.ResourceError {
Expand Down
28 changes: 28 additions & 0 deletions tpl/partials/integration_test.go
Expand Up @@ -324,3 +324,31 @@ timeout = '200ms'
b.Assert(err.Error(), qt.Contains, "timed out")

}

// See Issue #10789
func TestReturnExecuteFromTemplateInPartial(t *testing.T) {
t.Parallel()

files := `
-- config.toml --
baseURL = 'http://example.com/'
-- layouts/index.html --
{{ $r := partial "foo" }}
FOO:{{ $r.Content }}
-- layouts/partials/foo.html --
{{ $r := §§{{ partial "bar" }}§§ | resources.FromString "bar.html" | resources.ExecuteAsTemplate "bar.html" . }}
{{ return $r }}
-- layouts/partials/bar.html --
BAR
`

b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: t,
TxtarString: files,
},
).Build()

b.AssertFileContent("public/index.html", "OO:BAR")

}
9 changes: 6 additions & 3 deletions tpl/partials/partials.go
Expand Up @@ -129,7 +129,10 @@ func (ns *Namespace) Include(ctx context.Context, name string, contextList ...an
}

func (ns *Namespace) includWithTimeout(ctx context.Context, name string, dataList ...any) includeResult {
ctx, cancel := context.WithTimeout(ctx, ns.deps.Timeout)
// There are situation where the ctx we pass on to the partial lives longer than
// the partial itself. For example, when the partial returns the result from reosurces.ExecuteAsTemplate.
// Because of that, create a completely new context here.
timeoutCtx, cancel := context.WithTimeout(context.Background(), ns.deps.Timeout)
defer cancel()

res := make(chan includeResult, 1)
Expand All @@ -141,8 +144,8 @@ func (ns *Namespace) includWithTimeout(ctx context.Context, name string, dataLis
select {
case r := <-res:
return r
case <-ctx.Done():
err := ctx.Err()
case <-timeoutCtx.Done():
err := timeoutCtx.Err()
if err == context.DeadlineExceeded {
err = fmt.Errorf("partial %q timed out after %s. This is most likely due to infinite recursion. If this is just a slow template, you can try to increase the 'timeout' config setting.", name, ns.deps.Timeout)
}
Expand Down

0 comments on commit 3bbeb56

Please sign in to comment.