Просмотр исходного кода

fix(template): avoid race when rendering concurrently

Render bound per-request, language-specific functions (t, plural, elapsed)
onto the shared template before executing it. Doing that while other
goroutines execute the same template races on its function map and can
render a response using another request's language.

Clone the template before binding those functions so each request executes
its own copy.

Fixes #4380
Salih Muhammed 23 часов назад
Родитель
Сommit
56d0b31cb6
2 измененных файлов с 69 добавлено и 0 удалено
  1. 7 0
      internal/template/engine.go
  2. 62 0
      internal/template/engine_test.go

+ 7 - 0
internal/template/engine.go

@@ -94,6 +94,13 @@ func (e *Engine) Render(name string, data map[string]any) []byte {
 		panic("The template " + name + " does not exists.")
 	}
 
+	// Clone the template so the per-request, language-specific functions below
+	// are bound on a private copy. The shared template stored in e.templates is
+	// only ever cloned (never executed directly), so concurrent requests no
+	// longer race on its function map, which previously could cause a response
+	// to be rendered with another concurrent request's language.
+	tpl = template.Must(tpl.Clone())
+
 	printer := locale.NewPrinter(data["language"].(string))
 
 	// Functions that need to be declared at runtime.

+ 62 - 0
internal/template/engine_test.go

@@ -0,0 +1,62 @@
+// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package template // import "miniflux.app/v2/internal/template"
+
+import (
+	"bytes"
+	"sync"
+	"testing"
+)
+
+// TestRenderConcurrency renders the same template concurrently in different
+// languages. Because Render binds per-request, language-specific functions
+// ("t", "plural", "elapsed") onto the template, doing so on a shared template
+// while other requests execute it corrupts the output: a request can be served
+// another request's language. Each concurrent render must match the output of
+// the equivalent sequential render for its language.
+func TestRenderConcurrency(t *testing.T) {
+	engine := NewEngine("")
+	engine.ParseTemplates()
+
+	languages := []string{"en_US", "fr_FR", "de_DE", "es_ES", "pt_BR", "ru_RU", "zh_CN", "it_IT"}
+
+	newData := func(language string) map[string]any {
+		return map[string]any{"language": language, "theme": "system_serif"}
+	}
+
+	// Establish the expected output for each language sequentially.
+	expected := make(map[string][]byte, len(languages))
+	for _, language := range languages {
+		expected[language] = engine.Render("offline.html", newData(language))
+	}
+
+	const iterations = 300
+
+	var wg sync.WaitGroup
+	var mu sync.Mutex
+	mismatches := make(map[string]int)
+
+	for i := 0; i < iterations; i++ {
+		language := languages[i%len(languages)]
+		wg.Add(1)
+		go func(language string) {
+			defer wg.Done()
+			got := engine.Render("offline.html", newData(language))
+			if !bytes.Equal(got, expected[language]) {
+				mu.Lock()
+				mismatches[language]++
+				mu.Unlock()
+			}
+		}(language)
+	}
+	wg.Wait()
+
+	if len(mismatches) > 0 {
+		total := 0
+		for _, n := range mismatches {
+			total += n
+		}
+		t.Fatalf("concurrent Render produced wrong output for %d/%d requests (wrong-language translations); per-language mismatches: %v", total, iterations, mismatches)
+	}
+}