mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-14 20:37:55 -05:00
Fix concurrent map read and map write in short page lookups
Regression introduced in Hugo `v0.137.0`. Fixes #13019
This commit is contained in:
parent
2c3efc8106
commit
95e2d5beb8
5 changed files with 170 additions and 50 deletions
|
@ -13,11 +13,14 @@
|
||||||
|
|
||||||
package maps
|
package maps
|
||||||
|
|
||||||
import "sync"
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
// Cache is a simple thread safe cache backed by a map.
|
// Cache is a simple thread safe cache backed by a map.
|
||||||
type Cache[K comparable, T any] struct {
|
type Cache[K comparable, T any] struct {
|
||||||
m map[K]T
|
m map[K]T
|
||||||
|
hasBeenInitialized bool
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,11 +37,16 @@ func (c *Cache[K, T]) Get(key K) (T, bool) {
|
||||||
return zero, false
|
return zero, false
|
||||||
}
|
}
|
||||||
c.RLock()
|
c.RLock()
|
||||||
v, found := c.m[key]
|
v, found := c.get(key)
|
||||||
c.RUnlock()
|
c.RUnlock()
|
||||||
return v, found
|
return v, found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Cache[K, T]) get(key K) (T, bool) {
|
||||||
|
v, found := c.m[key]
|
||||||
|
return v, found
|
||||||
|
}
|
||||||
|
|
||||||
// GetOrCreate gets the value for the given key if it exists, or creates it if not.
|
// GetOrCreate gets the value for the given key if it exists, or creates it if not.
|
||||||
func (c *Cache[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
|
func (c *Cache[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
|
||||||
c.RLock()
|
c.RLock()
|
||||||
|
@ -61,13 +69,49 @@ func (c *Cache[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
|
||||||
return v, nil
|
return v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InitAndGet initializes the cache if not already done and returns the value for the given key.
|
||||||
|
// The init state will be reset on Reset or Drain.
|
||||||
|
func (c *Cache[K, T]) InitAndGet(key K, init func(get func(key K) (T, bool), set func(key K, value T)) error) (T, error) {
|
||||||
|
var v T
|
||||||
|
c.RLock()
|
||||||
|
if !c.hasBeenInitialized {
|
||||||
|
c.RUnlock()
|
||||||
|
if err := func() error {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
// Double check in case another goroutine has initialized it in the meantime.
|
||||||
|
if !c.hasBeenInitialized {
|
||||||
|
err := init(c.get, c.set)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.hasBeenInitialized = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
// Reacquire the read lock.
|
||||||
|
c.RLock()
|
||||||
|
}
|
||||||
|
|
||||||
|
v = c.m[key]
|
||||||
|
c.RUnlock()
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Set sets the given key to the given value.
|
// Set sets the given key to the given value.
|
||||||
func (c *Cache[K, T]) Set(key K, value T) {
|
func (c *Cache[K, T]) Set(key K, value T) {
|
||||||
c.Lock()
|
c.Lock()
|
||||||
c.m[key] = value
|
c.set(key, value)
|
||||||
c.Unlock()
|
c.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Cache[K, T]) set(key K, value T) {
|
||||||
|
c.m[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
// ForEeach calls the given function for each key/value pair in the cache.
|
// ForEeach calls the given function for each key/value pair in the cache.
|
||||||
func (c *Cache[K, T]) ForEeach(f func(K, T)) {
|
func (c *Cache[K, T]) ForEeach(f func(K, T)) {
|
||||||
c.RLock()
|
c.RLock()
|
||||||
|
@ -81,6 +125,7 @@ func (c *Cache[K, T]) Drain() map[K]T {
|
||||||
c.Lock()
|
c.Lock()
|
||||||
m := c.m
|
m := c.m
|
||||||
c.m = make(map[K]T)
|
c.m = make(map[K]T)
|
||||||
|
c.hasBeenInitialized = false
|
||||||
c.Unlock()
|
c.Unlock()
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
@ -94,6 +139,7 @@ func (c *Cache[K, T]) Len() int {
|
||||||
func (c *Cache[K, T]) Reset() {
|
func (c *Cache[K, T]) Reset() {
|
||||||
c.Lock()
|
c.Lock()
|
||||||
c.m = make(map[K]T)
|
c.m = make(map[K]T)
|
||||||
|
c.hasBeenInitialized = false
|
||||||
c.Unlock()
|
c.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,6 @@ import (
|
||||||
"github.com/gohugoio/hugo/hugolib/doctree"
|
"github.com/gohugoio/hugo/hugolib/doctree"
|
||||||
"github.com/gohugoio/hugo/hugolib/pagesfromdata"
|
"github.com/gohugoio/hugo/hugolib/pagesfromdata"
|
||||||
"github.com/gohugoio/hugo/identity"
|
"github.com/gohugoio/hugo/identity"
|
||||||
"github.com/gohugoio/hugo/lazy"
|
|
||||||
"github.com/gohugoio/hugo/media"
|
"github.com/gohugoio/hugo/media"
|
||||||
"github.com/gohugoio/hugo/output"
|
"github.com/gohugoio/hugo/output"
|
||||||
"github.com/gohugoio/hugo/resources"
|
"github.com/gohugoio/hugo/resources"
|
||||||
|
@ -925,59 +924,58 @@ func newPageMap(i int, s *Site, mcache *dynacache.Cache, pageTrees *pageTrees) *
|
||||||
s: s,
|
s: s,
|
||||||
}
|
}
|
||||||
|
|
||||||
m.pageReverseIndex = &contentTreeReverseIndex{
|
m.pageReverseIndex = newContentTreeTreverseIndex(func(get func(key any) (contentNodeI, bool), set func(key any, val contentNodeI)) {
|
||||||
initFn: func(rm map[any]contentNodeI) {
|
add := func(k string, n contentNodeI) {
|
||||||
add := func(k string, n contentNodeI) {
|
existing, found := get(k)
|
||||||
existing, found := rm[k]
|
if found && existing != ambiguousContentNode {
|
||||||
if found && existing != ambiguousContentNode {
|
set(k, ambiguousContentNode)
|
||||||
rm[k] = ambiguousContentNode
|
} else if !found {
|
||||||
} else if !found {
|
set(k, n)
|
||||||
rm[k] = n
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w := &doctree.NodeShiftTreeWalker[contentNodeI]{
|
||||||
|
Tree: m.treePages,
|
||||||
|
LockType: doctree.LockTypeRead,
|
||||||
|
Handle: func(s string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
|
||||||
|
p := n.(*pageState)
|
||||||
|
if p.PathInfo() != nil {
|
||||||
|
add(p.PathInfo().BaseNameNoIdentifier(), p)
|
||||||
}
|
}
|
||||||
}
|
return false, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
w := &doctree.NodeShiftTreeWalker[contentNodeI]{
|
if err := w.Walk(context.Background()); err != nil {
|
||||||
Tree: m.treePages,
|
panic(err)
|
||||||
LockType: doctree.LockTypeRead,
|
}
|
||||||
Handle: func(s string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
|
})
|
||||||
p := n.(*pageState)
|
|
||||||
if p.PathInfo() != nil {
|
|
||||||
add(p.PathInfo().BaseNameNoIdentifier(), p)
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := w.Walk(context.Background()); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
contentTreeReverseIndexMap: &contentTreeReverseIndexMap{},
|
|
||||||
}
|
|
||||||
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newContentTreeTreverseIndex(init func(get func(key any) (contentNodeI, bool), set func(key any, val contentNodeI))) *contentTreeReverseIndex {
|
||||||
|
return &contentTreeReverseIndex{
|
||||||
|
initFn: init,
|
||||||
|
mm: maps.NewCache[any, contentNodeI](),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type contentTreeReverseIndex struct {
|
type contentTreeReverseIndex struct {
|
||||||
initFn func(rm map[any]contentNodeI)
|
initFn func(get func(key any) (contentNodeI, bool), set func(key any, val contentNodeI))
|
||||||
*contentTreeReverseIndexMap
|
mm *maps.Cache[any, contentNodeI]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *contentTreeReverseIndex) Reset() {
|
func (c *contentTreeReverseIndex) Reset() {
|
||||||
c.init.ResetWithLock().Unlock()
|
c.mm.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *contentTreeReverseIndex) Get(key any) contentNodeI {
|
func (c *contentTreeReverseIndex) Get(key any) contentNodeI {
|
||||||
c.init.Do(func() {
|
v, _ := c.mm.InitAndGet(key, func(get func(key any) (contentNodeI, bool), set func(key any, val contentNodeI)) error {
|
||||||
c.m = make(map[any]contentNodeI)
|
c.initFn(get, set)
|
||||||
c.initFn(c.contentTreeReverseIndexMap.m)
|
return nil
|
||||||
})
|
})
|
||||||
return c.m[key]
|
return v
|
||||||
}
|
|
||||||
|
|
||||||
type contentTreeReverseIndexMap struct {
|
|
||||||
init lazy.OnceMore
|
|
||||||
m map[any]contentNodeI
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type sitePagesAssembler struct {
|
type sitePagesAssembler struct {
|
||||||
|
|
|
@ -17,9 +17,11 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
qt "github.com/frankban/quicktest"
|
qt "github.com/frankban/quicktest"
|
||||||
|
"github.com/gohugoio/hugo/identity"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestContentMapSite(t *testing.T) {
|
func TestContentMapSite(t *testing.T) {
|
||||||
|
@ -396,3 +398,77 @@ irrelevant
|
||||||
"<loc>https://example.org/en/sitemap.xml</loc>",
|
"<loc>https://example.org/en/sitemap.xml</loc>",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContentTreeReverseIndex(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
c := qt.New(t)
|
||||||
|
|
||||||
|
pageReverseIndex := newContentTreeTreverseIndex(
|
||||||
|
func(get func(key any) (contentNodeI, bool), set func(key any, val contentNodeI)) {
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
key := fmt.Sprint(i)
|
||||||
|
set(key, &testContentNode{key: key})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
key := fmt.Sprint(i)
|
||||||
|
v := pageReverseIndex.Get(key)
|
||||||
|
c.Assert(v, qt.Not(qt.IsNil))
|
||||||
|
c.Assert(v.Path(), qt.Equals, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issue 13019.
|
||||||
|
func TestContentTreeReverseIndexPara(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
pageReverseIndex := newContentTreeTreverseIndex(
|
||||||
|
func(get func(key any) (contentNodeI, bool), set func(key any, val contentNodeI)) {
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
key := fmt.Sprint(i)
|
||||||
|
set(key, &testContentNode{key: key})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
for j := 0; j < 10; j++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(i int) {
|
||||||
|
defer wg.Done()
|
||||||
|
pageReverseIndex.Get(fmt.Sprint(i))
|
||||||
|
}(j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type testContentNode struct {
|
||||||
|
key string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *testContentNode) GetIdentity() identity.Identity {
|
||||||
|
return identity.StringIdentity(n.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *testContentNode) ForEeachIdentity(cb func(id identity.Identity) bool) bool {
|
||||||
|
panic("not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *testContentNode) Path() string {
|
||||||
|
return n.key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *testContentNode) isContentNodeBranch() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *testContentNode) resetBuildState() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *testContentNode) MarkStale() {
|
||||||
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ type Init struct {
|
||||||
prev *Init
|
prev *Init
|
||||||
children []*Init
|
children []*Init
|
||||||
|
|
||||||
init OnceMore
|
init onceMore
|
||||||
out any
|
out any
|
||||||
err error
|
err error
|
||||||
f func(context.Context) (any, error)
|
f func(context.Context) (any, error)
|
||||||
|
|
10
lazy/once.go
10
lazy/once.go
|
@ -24,13 +24,13 @@ import (
|
||||||
// * it can be reset, so the action can be repeated if needed
|
// * it can be reset, so the action can be repeated if needed
|
||||||
// * it has methods to check if it's done or in progress
|
// * it has methods to check if it's done or in progress
|
||||||
|
|
||||||
type OnceMore struct {
|
type onceMore struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
lock uint32
|
lock uint32
|
||||||
done uint32
|
done uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *OnceMore) Do(f func()) {
|
func (t *onceMore) Do(f func()) {
|
||||||
if atomic.LoadUint32(&t.done) == 1 {
|
if atomic.LoadUint32(&t.done) == 1 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -53,15 +53,15 @@ func (t *OnceMore) Do(f func()) {
|
||||||
f()
|
f()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *OnceMore) InProgress() bool {
|
func (t *onceMore) InProgress() bool {
|
||||||
return atomic.LoadUint32(&t.lock) == 1
|
return atomic.LoadUint32(&t.lock) == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *OnceMore) Done() bool {
|
func (t *onceMore) Done() bool {
|
||||||
return atomic.LoadUint32(&t.done) == 1
|
return atomic.LoadUint32(&t.done) == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *OnceMore) ResetWithLock() *sync.Mutex {
|
func (t *onceMore) ResetWithLock() *sync.Mutex {
|
||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
defer atomic.StoreUint32(&t.done, 0)
|
defer atomic.StoreUint32(&t.done, 0)
|
||||||
return &t.mu
|
return &t.mu
|
||||||
|
|
Loading…
Reference in a new issue