Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions pkg/statistics/handle/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,9 @@ func (h *Handle) initStatsHistogramsLite(ctx context.Context, cache statstypes.S
break
}
h.initStatsHistograms4ChunkLite(cache, iter)
// The same table may continue in the next chunk. Drain LFU async admission/rejection
// before the next chunk reads or mutates it again.
cache.WaitForAsyncUpdates()
}
return nil
}
Expand All @@ -356,7 +359,14 @@ func (h *Handle) initStatsHistograms(is infoschema.InfoSchema, cache statstypes.
if req.NumRows() == 0 {
break
}
<<<<<<< HEAD
h.initStatsHistograms4Chunk(is, cache, iter, false)
=======
h.initStatsHistograms4Chunk(is, cache, iter, isFullCache(cache, totalMemory))
// The same table may continue in the next chunk. Drain LFU async admission/rejection
// before the next chunk reads or mutates it again.
cache.WaitForAsyncUpdates()
>>>>>>> 933e59fbe58 (statistics: stabilize TestNonLiteInitStatsWithTableIDs case (#67971))
Comment on lines +362 to +369

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Resolve the remaining cherry-pick conflicts before merge.

The <<<<<<<, =======, and >>>>>>> markers make this package uncompilable; the incoming alternatives also reference signatures and variables not present in the selected release branch.

  • pkg/statistics/handle/bootstrap.go#L362-L369: reconcile histogram initialization and retain the intended per-chunk synchronization.
  • pkg/statistics/handle/bootstrap.go#L498-L505: reconcile the TopN call with the release branch’s actual helper signature.
  • pkg/statistics/handle/bootstrap.go#L814-L832: reconcile targeted-cache publication with the release branch API.
  • pkg/statistics/handle/bootstrap.go#L904-L926: reconcile final cache publication and cleanup behavior.
📍 Affects 1 file
  • pkg/statistics/handle/bootstrap.go#L362-L369 (this comment)
  • pkg/statistics/handle/bootstrap.go#L498-L505
  • pkg/statistics/handle/bootstrap.go#L814-L832
  • pkg/statistics/handle/bootstrap.go#L904-L926
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/statistics/handle/bootstrap.go` around lines 362 - 369, Remove all
cherry-pick conflict markers and reconcile the bootstrap flow with the selected
release branch. In pkg/statistics/handle/bootstrap.go:362-369, use the
release-compatible initStatsHistograms4Chunk call while retaining per-chunk
WaitForAsyncUpdates synchronization; at 498-505, adapt the TopN invocation to
the branch’s actual helper signature; at 814-832 and 904-926, update
targeted-cache and final-cache publication plus cleanup to the release branch
APIs and behavior.

}
return nil
}
Expand Down Expand Up @@ -485,7 +495,14 @@ func (h *Handle) initStatsTopN(cache statstypes.StatsCache, totalMemory uint64)
if req.NumRows() == 0 {
break
}
<<<<<<< HEAD
h.initStatsTopN4Chunk(cache, iter, totalMemory)
=======
h.initStatsTopN4Chunk(cache, iter, totalMemory, tablesWithBuckets)
// The same table may continue in the next chunk. Drain LFU async admission/rejection
// before the next chunk reads or mutates it again.
cache.WaitForAsyncUpdates()
>>>>>>> 933e59fbe58 (statistics: stabilize TestNonLiteInitStatsWithTableIDs case (#67971))
}
return nil
}
Expand Down Expand Up @@ -726,6 +743,9 @@ func (h *Handle) initStatsBucketsByPaging(cache statstypes.StatsCache, task init
break
}
h.initStatsBuckets4Chunk(cache, iter)
// The same table may continue in the next chunk. Drain LFU async admission/rejection
// before the next chunk reads or mutates it again.
cache.WaitForAsyncUpdates()
}
return nil
}
Expand Down Expand Up @@ -780,6 +800,9 @@ func (h *Handle) InitStatsLite(ctx context.Context) (err error) {
if err != nil {
return errors.Trace(err)
}
// Required: initStatsMeta adds new tables without an internal wait; histogram loading
// reads them immediately.
cache.WaitForAsyncUpdates()
statslogutil.StatsLogger().Info("Complete loading the stats meta in the lite mode", zap.Duration("duration", time.Since(start)))
start = time.Now()
err = h.initStatsHistogramsLite(ctx, cache)
Expand All @@ -788,7 +811,25 @@ func (h *Handle) InitStatsLite(ctx context.Context) (err error) {
return errors.Trace(err)
}
statslogutil.StatsLogger().Info("Complete loading the histogram in the lite mode", zap.Duration("duration", time.Since(start)))
<<<<<<< HEAD
h.Replace(cache)
=======
// If tableIDs is empty, it means we load all the tables' stats meta and histograms.
// So we can replace the global cache with the new cache.
if len(tableIDs) == 0 {
h.Replace(cache)
} else {
tables := cache.Values()
for _, table := range tables {
intest.Assert(table != nil, "table should not be nil")
h.Put(table.PhysicalID, table)
}
// Targeted refresh publishes refreshed tables to global LFU; make async admissions visible before returning.
h.StatsCache.WaitForAsyncUpdates()
// Do not forget to close the new cache. Otherwise it would cause the goroutine leak issue.
cache.Close()
}
>>>>>>> 933e59fbe58 (statistics: stabilize TestNonLiteInitStatsWithTableIDs case (#67971))
return nil
}

Expand Down Expand Up @@ -820,6 +861,9 @@ func (h *Handle) InitStats(ctx context.Context, is infoschema.InfoSchema) (err e
if err != nil {
return errors.Trace(err)
}
// Required: initStatsMeta adds new tables without an internal wait; histogram loading
// reads them immediately.
cache.WaitForAsyncUpdates()
statslogutil.StatsLogger().Info("Complete loading the stats meta", zap.Duration("duration", time.Since(start)))
initstats.InitStatsPercentage.Store(initStatsPercentageInterval)
start = time.Now()
Expand Down Expand Up @@ -857,7 +901,29 @@ func (h *Handle) InitStats(ctx context.Context, is infoschema.InfoSchema) (err e
if err != nil {
return errors.Trace(err)
}
<<<<<<< HEAD
h.Replace(cache)
=======
// CalcPreScalar writes tables back; drain before replacing/publishing the cache.
cache.WaitForAsyncUpdates()
statslogutil.StatsLogger().Info("Complete loading the bucket", zap.Duration("duration", time.Since(start)))
Comment on lines +907 to +909

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Drain the direct bucket-loading loop between chunks.

This final drain occurs after all chunks, so it cannot prevent a following initStatsBuckets4Chunk call from reading a stale snapshot written by the preceding chunk. Add the same per-chunk wait used by initStatsBucketsByPaging after line 703.

Proposed fix
 		h.initStatsBuckets4Chunk(cache, iter)
+		cache.WaitForAsyncUpdates()
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/statistics/handle/bootstrap.go` around lines 907 - 909, Update the direct
bucket-loading loop in the bootstrap flow to call cache.WaitForAsyncUpdates()
after each chunk is processed, before the next initStatsBuckets4Chunk invocation
can read the cache. Match the per-chunk wait placement used by
initStatsBucketsByPaging, while retaining the existing final drain and
completion log.


// If tableIDs is empty, it means we load all the tables' stats.
// So we can replace the global cache with the new cache.
if len(tableIDs) == 0 {
h.Replace(cache)
} else {
tables := cache.Values()
for _, table := range tables {
intest.Assert(table != nil, "table should not be nil")
h.Put(table.PhysicalID, table)
}
// Targeted refresh publishes refreshed tables to global LFU; make async admissions visible before returning.
h.StatsCache.WaitForAsyncUpdates()
// Do not forget to close the new cache. Otherwise it would cause the goroutine leak issue.
cache.Close()
}
>>>>>>> 933e59fbe58 (statistics: stabilize TestNonLiteInitStatsWithTableIDs case (#67971))
return nil
}

Expand Down
4 changes: 4 additions & 0 deletions pkg/statistics/handle/cache/internal/inner.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,8 @@ type StatsCacheInner interface {
Close()
// TriggerEvict triggers the cache to evict some items
TriggerEvict()
// WaitForAsyncUpdates blocks until buffered asynchronous cache writes are visible to later Get calls.
// Use it after adding new items when following reads depend on them. LFU/Ristretto admits
// non-resident items asynchronously; init stats calls this between load phases/chunks.
WaitForAsyncUpdates()
}
40 changes: 34 additions & 6 deletions pkg/statistics/handle/cache/internal/lfu/lfu_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ import (
"golang.org/x/exp/rand"
)

// LFU is a LFU based on the ristretto.Cache
// LFU is a LFU based on the ristretto.Cache.
//
// NOTE: In Ristretto, updates to keys already resident in the primary store become visible
// immediately, but keys not currently resident go through a buffered admission path and are
// applied by a background worker. So a successful Set does not guarantee immediate Get
// visibility for a non-resident key.
type LFU struct {
cache *ristretto.Cache
// This is a secondary cache layer used to store all tables,
Expand Down Expand Up @@ -87,7 +92,27 @@ func adjustMemCost(totalMemCost int64) (result int64, err error) {
return totalMemCost, nil
}

// Get implements statsCacheInner
// Get reads the primary Ristretto cache first and falls back to resultKeySet.
//
// NOTE: We keep this order to preserve Ristretto's admission/frequency behavior.
// That means a resident primary-cache entry wins even if resultKeySet has already been updated to
// a newer table snapshot; until the buffered Ristretto write catches up, Get can still observe
// that older snapshot. We are aware of this stale-read window, but it is very rare in practice, so
// we currently document it instead of changing the read order and taking on different concurrency
// trade-offs.
//
// Extreme example (the old flaky pattern from TestNonLiteInitStatsWithTableIDs):
// 1. InitStats(tbl1) publishes tbl1 to resultKeySet, but the new primary-cache entry is still in
// Ristretto's buffered admission path.
// 2. A later targeted InitStats call that includes tbl1 again reads tbl1 from resultKeySet,
// fills in more index state, and republishes a newer tbl1 snapshot there.
// 3. The older tbl1 can still reach Ristretto first, so topn/buckets sees that resident older
// snapshot, misses the index entry, and Put() can write that stale tbl1 back into the main
// cache.
//
// For phase-by-phase or chunk-by-chunk cache construction, callers that read after writes should
// call WaitForAsyncUpdates before the next dependent read. InitStats does this between load phases
// and chunks so a temporary LFU cache cannot carry an older table snapshot into later loading.
func (s *LFU) Get(tid int64) (*statistics.Table, bool) {
result, ok := s.cache.Get(tid)
if !ok {
Expand All @@ -96,7 +121,10 @@ func (s *LFU) Get(tid int64) (*statistics.Table, bool) {
return result.(*statistics.Table), ok
}

// Put implements statsCacheInner
// Put publishes to resultKeySet first, then hands the item to Ristretto.
//
// Resident-key updates become visible there immediately; non-resident keys still go through the
// buffered admission path.
func (s *LFU) Put(tblID int64, tbl *statistics.Table) bool {
cost := tbl.MemoryUsage().TotalTrackingMemUsage()
s.resultKeySet.AddKeyValue(tblID, tbl)
Expand Down Expand Up @@ -221,9 +249,9 @@ func (s *LFU) SetCapacity(maxCost int64) {
metrics.CostGauge.Set(float64(s.Cost()))
}

// wait blocks until all buffered writes have been applied. This ensures a call to Set()
// will be visible to future calls to Get(). it is only used for test.
func (s *LFU) wait() {
// WaitForAsyncUpdates blocks until all buffered writes have been applied. This ensures a call to
// Put is visible to future calls to Get.
func (s *LFU) WaitForAsyncUpdates() {
s.cache.Wait()
}

Expand Down
38 changes: 19 additions & 19 deletions pkg/statistics/handle/cache/internal/lfu/lfu_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ func TestLFUPutGetDel(t *testing.T) {
mockTable := testutil.NewMockStatisticsTable(1, 1, true, false, false)
mockTableID := int64(1)
lfu.Put(mockTableID, mockTable)
lfu.wait()
lfu.WaitForAsyncUpdates()
lfu.Del(mockTableID)
v, ok := lfu.Get(mockTableID)
require.False(t, ok)
require.Nil(t, v)
lfu.wait()
lfu.WaitForAsyncUpdates()
require.Equal(t, uint64(lfu.Cost()), lfu.metrics().CostAdded()-lfu.metrics().CostEvicted())
require.Equal(t, 0, len(lfu.Values()))
}
Expand All @@ -58,15 +58,15 @@ func TestLFUFreshMemUsage(t *testing.T) {
lfu.Put(int64(1), t1)
lfu.Put(int64(2), t2)
lfu.Put(int64(3), t3)
lfu.wait()
lfu.WaitForAsyncUpdates()
require.Equal(t, lfu.Cost(), 6*mockCMSMemoryUsage+6*mockCMSMemoryUsage)
t4 := testutil.NewMockStatisticsTable(2, 1, true, false, false)
lfu.Put(int64(1), t4)
lfu.wait()
lfu.WaitForAsyncUpdates()
require.Equal(t, lfu.Cost(), 7*mockCMSMemoryUsage+6*mockCMSMemoryUsage)
t5 := testutil.NewMockStatisticsTable(2, 2, true, false, false)
lfu.Put(int64(1), t5)
lfu.wait()
lfu.WaitForAsyncUpdates()
require.Equal(t, lfu.Cost(), 7*mockCMSMemoryUsage+7*mockCMSMemoryUsage)

t6 := testutil.NewMockStatisticsTable(1, 2, true, false, false)
Expand All @@ -76,7 +76,7 @@ func TestLFUFreshMemUsage(t *testing.T) {
t7 := testutil.NewMockStatisticsTable(1, 1, true, false, false)
lfu.Put(int64(1), t7)
require.Equal(t, lfu.Cost(), 6*mockCMSMemoryUsage+6*mockCMSMemoryUsage)
lfu.wait()
lfu.WaitForAsyncUpdates()
require.Equal(t, uint64(lfu.Cost()), lfu.metrics().CostAdded()-lfu.metrics().CostEvicted())
}

Expand All @@ -88,7 +88,7 @@ func TestLFUPutTooBig(t *testing.T) {
lfu.Put(int64(1), mockTable)
_, ok := lfu.Get(int64(1))
require.True(t, ok)
lfu.wait()
lfu.WaitForAsyncUpdates()
require.Equal(t, uint64(lfu.Cost()), lfu.metrics().CostAdded()-lfu.metrics().CostEvicted())
}

Expand All @@ -102,14 +102,14 @@ func TestCacheLen(t *testing.T) {
t2 := testutil.NewMockStatisticsTable(1, 1, true, false, false)
// put t2, t1 should be evicted 2 items and still exists in the list
lfu.Put(int64(2), t2)
lfu.wait()
lfu.WaitForAsyncUpdates()
require.Equal(t, lfu.Len(), 2)
require.Equal(t, uint64(8), lfu.metrics().CostAdded()-lfu.metrics().CostEvicted())

// put t3, t1/t2 should be evicted all items. but t1/t2 still exists in the list
t3 := testutil.NewMockStatisticsTable(2, 1, true, false, false)
lfu.Put(int64(3), t3)
lfu.wait()
lfu.WaitForAsyncUpdates()
require.Equal(t, lfu.Len(), 3)
require.Equal(t, uint64(12), lfu.metrics().CostAdded()-lfu.metrics().CostEvicted())
}
Expand All @@ -133,7 +133,7 @@ func TestLFUCachePutGetWithManyConcurrency(t *testing.T) {
}(i)
}
wg.Wait()
lfu.wait()
lfu.WaitForAsyncUpdates()
require.Equal(t, lfu.Len(), 1000)
require.Equal(t, uint64(lfu.Cost()), lfu.metrics().CostAdded()-lfu.metrics().CostEvicted())
require.Equal(t, 1000, len(lfu.Values()))
Expand Down Expand Up @@ -164,7 +164,7 @@ func TestLFUCachePutGetWithManyConcurrency2(t *testing.T) {
}()
}
wg.Wait()
lfu.wait()
lfu.WaitForAsyncUpdates()
require.Equal(t, uint64(lfu.Cost()), lfu.metrics().CostAdded()-lfu.metrics().CostEvicted())
require.Equal(t, 1000, len(lfu.Values()))
}
Expand Down Expand Up @@ -202,7 +202,7 @@ func TestLFUCachePutGetWithManyConcurrencyAndSmallConcurrency(t *testing.T) {
}()
}
wg.Wait()
lfu.wait()
lfu.WaitForAsyncUpdates()
v, ok := lfu.Get(rand.Int63n(50))
require.True(t, ok)
v.ForEachColumnImmutable(func(_ int64, c *statistics.Column) bool {
Expand Down Expand Up @@ -245,14 +245,14 @@ func TestLFUReject(t *testing.T) {
t1 := testutil.NewMockStatisticsTable(2, 1, true, false, false)
require.Equal(t, 2*mockCMSMemoryUsage+mockCMSMemoryUsage, t1.MemoryUsage().TotalTrackingMemUsage())
lfu.Put(1, t1)
lfu.wait()
lfu.WaitForAsyncUpdates()
require.Equal(t, lfu.Cost(), 2*mockCMSMemoryUsage+mockCMSMemoryUsage)

lfu.SetCapacity(2*mockCMSMemoryUsage + mockCMSMemoryUsage - 1)

t2 := testutil.NewMockStatisticsTable(2, 1, true, false, false)
require.True(t, lfu.Put(2, t2))
lfu.wait()
lfu.WaitForAsyncUpdates()
time.Sleep(3 * time.Second)
require.Equal(t, int64(0), lfu.Cost())
require.Len(t, lfu.Values(), 2)
Expand All @@ -275,7 +275,7 @@ func TestMemoryControl(t *testing.T) {
t1 := testutil.NewMockStatisticsTable(2, 1, true, false, false)
require.Equal(t, 2*mockCMSMemoryUsage+mockCMSMemoryUsage, t1.MemoryUsage().TotalTrackingMemUsage())
lfu.Put(1, t1)
lfu.wait()
lfu.WaitForAsyncUpdates()

for i := 2; i <= 1000; i++ {
t1 := testutil.NewMockStatisticsTable(2, 1, true, false, false)
Expand All @@ -286,19 +286,19 @@ func TestMemoryControl(t *testing.T) {

for i := 1000; i > 990; i-- {
lfu.SetCapacity(int64(i-1) * (2*mockCMSMemoryUsage + mockCMSMemoryUsage))
lfu.wait()
lfu.WaitForAsyncUpdates()
require.Equal(t, int64(i-1)*(2*mockCMSMemoryUsage+mockCMSMemoryUsage), lfu.Cost())
}
for i := 990; i > 100; i = i - 100 {
lfu.SetCapacity(int64(i-1) * (2*mockCMSMemoryUsage + mockCMSMemoryUsage))
lfu.wait()
lfu.WaitForAsyncUpdates()
require.Equal(t, int64(i-1)*(2*mockCMSMemoryUsage+mockCMSMemoryUsage), lfu.Cost())
}
lfu.SetCapacity(int64(10) * (2*mockCMSMemoryUsage + mockCMSMemoryUsage))
lfu.wait()
lfu.WaitForAsyncUpdates()
require.Equal(t, int64(10)*(2*mockCMSMemoryUsage+mockCMSMemoryUsage), lfu.Cost())
lfu.SetCapacity(0)
lfu.wait()
lfu.WaitForAsyncUpdates()
require.Equal(t, int64(10)*(2*mockCMSMemoryUsage+mockCMSMemoryUsage), lfu.Cost())
}

Expand Down
3 changes: 3 additions & 0 deletions pkg/statistics/handle/cache/internal/mapcache/map_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,6 @@ func (*MapCache) Close() {}

// TriggerEvict implements statsCacheInner
func (*MapCache) TriggerEvict() {}

// WaitForAsyncUpdates implements statsCacheInner.
func (*MapCache) WaitForAsyncUpdates() {}
5 changes: 5 additions & 0 deletions pkg/statistics/handle/cache/statscache.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,11 @@ func (s *StatsCacheImpl) TriggerEvict() {
s.Load().TriggerEvict()
}

// WaitForAsyncUpdates blocks until buffered asynchronous cache writes are visible to later Get calls.
func (s *StatsCacheImpl) WaitForAsyncUpdates() {
s.Load().WaitForAsyncUpdates()
}

// MaxTableStatsVersion returns the version of the current cache, which is defined as
// the max table stats version the cache has in its lifecycle.
func (s *StatsCacheImpl) MaxTableStatsVersion() uint64 {
Expand Down
4 changes: 4 additions & 0 deletions pkg/statistics/handle/cache/statscache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,7 @@ func (*mockStatsCacheInner) Close() {
func (*mockStatsCacheInner) TriggerEvict() {
panic("not implemented")
}

func (*mockStatsCacheInner) WaitForAsyncUpdates() {
panic("not implemented")
}
5 changes: 5 additions & 0 deletions pkg/statistics/handle/cache/statscacheinner.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,8 @@ func (sc *StatsCache) Update(tables []*statistics.Table, deletedIDs []int64, ski
func (sc *StatsCache) TriggerEvict() {
sc.c.TriggerEvict()
}

// WaitForAsyncUpdates blocks until buffered asynchronous cache writes are visible to later Get calls.
func (sc *StatsCache) WaitForAsyncUpdates() {
sc.c.WaitForAsyncUpdates()
}
5 changes: 5 additions & 0 deletions pkg/statistics/handle/types/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,11 @@ type StatsCache interface {

// TriggerEvict triggers the cache to evict some items
TriggerEvict()

// WaitForAsyncUpdates blocks until buffered asynchronous cache writes are visible to later Get calls.
// Use it after adding new items when following reads depend on them. LFU/Ristretto admits
// non-resident items asynchronously; init stats calls this between load phases/chunks.
WaitForAsyncUpdates()
}

// StatsLockTable is the table info of which will be locked.
Expand Down