diff --git a/CHANGELOG.md b/CHANGELOG.md index 328621f..0ccf323 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## Unreleased +### Fixed + +- The `{% cycle %}` tag no longer assigns to an error variable shared by every + render of a parsed template, so a template containing `{% cycle %}` can be + rendered concurrently from multiple goroutines. + ## 1.9.2 (2026-08-16) ### Performance diff --git a/tags/iteration_tags.go b/tags/iteration_tags.go index b088d5c..3be50aa 100644 --- a/tags/iteration_tags.go +++ b/tags/iteration_tags.go @@ -62,7 +62,7 @@ func cycleTag(args string) (func(io.Writer, render.Context) error, error) { n := cycleMap[group] cycleMap[group] = n + 1 // The parser guarantees that there will be at least one item. - _, err = io.WriteString(w, values[n%len(values)]) + _, err := io.WriteString(w, values[n%len(values)]) return err }, nil diff --git a/template_test.go b/template_test.go index 38a0770..9654533 100644 --- a/template_test.go +++ b/template_test.go @@ -115,6 +115,35 @@ func TestTemplate_Render_race(t *testing.T) { wg2.Wait() } +func TestTemplate_Render_cycle_race(t *testing.T) { + engine := NewEngine() + tpl, err := engine.ParseString(`{% for a in (1..4) %}{% cycle 'a', 'b' %}{% endfor %}`) + require.NoError(t, err) + + const ( + goroutines = 16 + renders = 50 + ) + + var wg sync.WaitGroup + + for range goroutines { + wg.Add(1) + + go func() { + defer wg.Done() + + for range renders { + out, err := tpl.RenderString(Bindings{}) + assert.NoError(t, err) + assert.Equal(t, "abab", out) + } + }() + } + + wg.Wait() +} + func BenchmarkTemplate_Render(b *testing.B) { engine := NewEngine() bindings := Bindings{"a": "string value"}