Skip to content

Commit 59e8ced

Browse files
committed
sql/hints: add statement hint cache generation
This commit adds a generation to the statement hints cache, which is incremented every time the cache processes a rangefeed event. The generation is used as part of the `cat.DependencyDigest`, used to decide whether an expensive staleness check is necessary for a cached plan. Informs #148160 Release note: None
1 parent 2eff7bd commit 59e8ced

File tree

5 files changed

+117
-4
lines changed

5 files changed

+117
-4
lines changed

pkg/sql/hints/hint_cache.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"context"
1010
"hash/fnv"
1111
"sync"
12+
"sync/atomic"
1213
"time"
1314

1415
"github.com/cockroachdb/cockroach/pkg/keys"
@@ -87,6 +88,10 @@ type StatementHintsCache struct {
8788

8889
// Used to read hints from the database when they are not in the cache.
8990
db descs.DB
91+
92+
// generation is incremented any time the hint cache is updated by the
93+
// rangefeed.
94+
generation atomic.Int64
9095
}
9196

9297
// cacheSize is the size of the entries to store in the cache.
@@ -214,6 +219,7 @@ func (c *StatementHintsCache) onUpdate(
214219
// handleInitialScan builds the hintedHashes map and adds it to
215220
// StatementHintsCache after the initial scan completes.
216221
func (c *StatementHintsCache) handleInitialScan(update rangefeedcache.Update[*bufferEvent]) {
222+
defer c.generation.Add(1)
217223
hintedHashes := make(map[int64]hlc.Timestamp)
218224
for _, ev := range update.Events {
219225
if ev.del {
@@ -246,6 +252,7 @@ func (c *StatementHintsCache) handleIncrementalUpdate(
246252
// Avoid synchronization when we're just bumping the resolved timestamp.
247253
return
248254
}
255+
defer c.generation.Add(1)
249256
c.mu.Lock()
250257
defer c.mu.Unlock()
251258
for _, ev := range update.Events {
@@ -309,8 +316,6 @@ func (c *StatementHintsCache) checkHashHasHintsAsync(
309316
// Unset the timestamp to indicate that the refresh is done.
310317
c.mu.hintedHashes[hash] = hlc.Timestamp{}
311318
} else {
312-
// There may be a hintCache entry with no hints; drop it.
313-
c.mu.hintCache.Del(hash)
314319
delete(c.mu.hintedHashes, hash)
315320
}
316321
return true
@@ -378,6 +383,12 @@ var _ rangefeedbuffer.Event = &bufferEvent{}
378383
// Reading from the cache.
379384
// ============================================================================
380385

386+
// GetGeneration returns the current generation, which will change if any
387+
// modifications happen to the cache.
388+
func (c *StatementHintsCache) GetGeneration() int64 {
389+
return c.generation.Load()
390+
}
391+
381392
// MaybeGetStatementHints attempts to retrieve the hints for the given statement
382393
// fingerprint. It returns nil if the statement has no hints, or there was an
383394
// error retrieving them.

pkg/sql/hints/hint_cache_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"context"
1010
"hash/fnv"
1111
"testing"
12+
"time"
1213

1314
"github.com/cockroachdb/cockroach/pkg/base"
1415
"github.com/cockroachdb/cockroach/pkg/kv/kvclient/rangefeed"
@@ -21,6 +22,7 @@ import (
2122
"github.com/cockroachdb/cockroach/pkg/testutils/testcluster"
2223
"github.com/cockroachdb/cockroach/pkg/util/leaktest"
2324
"github.com/cockroachdb/cockroach/pkg/util/log"
25+
"github.com/cockroachdb/cockroach/pkg/util/randutil"
2426
"github.com/cockroachdb/errors"
2527
"github.com/stretchr/testify/require"
2628
)
@@ -372,6 +374,98 @@ func TestHintCacheMultiTenant(t *testing.T) {
372374
require.Nil(t, hc2.MaybeGetStatementHints(ctx, fingerprintShared))
373375
}
374376

377+
// TestHintCacheGeneration tests that the cache generation is incremented
378+
// correctly when hints are added and removed, and that the generation is not
379+
// incremented when there are no updates.
380+
func TestHintCacheGeneration(t *testing.T) {
381+
defer leaktest.AfterTest(t)()
382+
defer log.Scope(t).Close(t)
383+
384+
ctx := context.Background()
385+
rnd, _ := randutil.NewTestRand()
386+
srv, db, _ := serverutils.StartServer(t, base.TestServerArgs{})
387+
defer srv.Stopper().Stop(ctx)
388+
ts := srv.ApplicationLayer()
389+
r := sqlutils.MakeSQLRunner(db)
390+
setTestDefaults(t, srv)
391+
392+
// Create a hints cache.
393+
hc := createHintsCache(t, ctx, ts)
394+
395+
// Helper that retrieves the generation and verifies that it doesn't change
396+
// over a short period.
397+
getGenerationAssertNoChange := func() int64 {
398+
t.Helper()
399+
startGeneration := hc.GetGeneration()
400+
time.Sleep(time.Duration(rnd.Intn(int(time.Millisecond))))
401+
require.Equal(t, startGeneration, hc.GetGeneration())
402+
return startGeneration
403+
}
404+
// Helper that waits until the generation increments from the given start
405+
// point.
406+
waitForGenerationInc := func(prevGeneration int64) {
407+
t.Helper()
408+
testutils.SucceedsSoon(t, func() error {
409+
t.Helper()
410+
if gen := hc.GetGeneration(); gen <= prevGeneration {
411+
return errors.Errorf("expected generation >= %d, got %d", prevGeneration, gen)
412+
}
413+
return nil
414+
})
415+
}
416+
417+
// The initial scan should increment the generation.
418+
waitForGenerationInc(0)
419+
generationAfterInitialScan := getGenerationAssertNoChange()
420+
421+
// Insert a hint - generation should increment.
422+
fingerprint1 := "SELECT a FROM t WHERE b = $1"
423+
insertStatementHint(t, r, fingerprint1)
424+
waitForGenerationInc(generationAfterInitialScan)
425+
waitForUpdateOnFingerprintHash(t, ctx, hc, fingerprint1, 1)
426+
generationAfterInsert := getGenerationAssertNoChange()
427+
428+
// Insert another hint for the same fingerprint - generation should increment
429+
// again.
430+
insertStatementHint(t, r, fingerprint1)
431+
waitForGenerationInc(generationAfterInsert)
432+
waitForUpdateOnFingerprintHash(t, ctx, hc, fingerprint1, 2)
433+
generationAfterSecondInsert := getGenerationAssertNoChange()
434+
435+
// Add a hint for a different fingerprint - generation should increment.
436+
fingerprint2 := "SELECT c FROM t WHERE d = $1"
437+
insertStatementHint(t, r, fingerprint2)
438+
waitForGenerationInc(generationAfterSecondInsert)
439+
waitForUpdateOnFingerprintHash(t, ctx, hc, fingerprint2, 1)
440+
generationAfterDifferentFingerprint := getGenerationAssertNoChange()
441+
442+
// Delete one hint - generation should increment.
443+
deleteStatementHints(t, r, fingerprint1, 1)
444+
waitForGenerationInc(generationAfterDifferentFingerprint)
445+
waitForUpdateOnFingerprintHash(t, ctx, hc, fingerprint1, 1)
446+
generationAfterDelete := getGenerationAssertNoChange()
447+
448+
// Delete all remaining hints for fingerprint1 - generation should increment.
449+
deleteStatementHints(t, r, fingerprint1, 0)
450+
waitForGenerationInc(generationAfterDelete)
451+
waitForUpdateOnFingerprintHash(t, ctx, hc, fingerprint1, 0)
452+
generationAfterDeleteAll := getGenerationAssertNoChange()
453+
454+
// Query for hints (cache access) should NOT increment generation.
455+
hc.MaybeGetStatementHints(ctx, fingerprint2)
456+
getGenerationAssertNoChange()
457+
458+
// Accessing a non-existent fingerprint should also NOT increment generation.
459+
hc.MaybeGetStatementHints(ctx, "SELECT nonexistent FROM t")
460+
getGenerationAssertNoChange()
461+
462+
// Delete all remaining hints.
463+
deleteStatementHints(t, r, fingerprint2, 0)
464+
waitForGenerationInc(generationAfterDeleteAll)
465+
waitForUpdateOnFingerprintHash(t, ctx, hc, fingerprint2, 0)
466+
getGenerationAssertNoChange()
467+
}
468+
375469
func setTestDefaults(t *testing.T, srv serverutils.TestServerInterface) {
376470
// These settings can only be set on the system tenant.
377471
r := sqlutils.MakeSQLRunner(srv.SystemLayer().SQLConn(t))

pkg/sql/opt/cat/catalog.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ type DependencyDigest struct {
9595
// StatsGeneration tracks if any new statistics have been published.
9696
StatsGeneration int64
9797

98+
// HintsGeneration tracks if any new hints have been published in
99+
// system.statement_hints.
100+
HintsGeneration int64
101+
98102
// SystemConfig tracks the current system config, which is refreshed on
99103
// any zone config update.
100104
SystemConfig *config.SystemConfig
@@ -108,6 +112,7 @@ type DependencyDigest struct {
108112
func (d *DependencyDigest) Equal(other *DependencyDigest) bool {
109113
return d.LeaseGeneration == other.LeaseGeneration &&
110114
d.StatsGeneration == other.StatsGeneration &&
115+
d.HintsGeneration == other.HintsGeneration &&
111116
// Note: If the system config is modified a new SystemConfig structure
112117
// is always allocated. Individual fields cannot change on the caller,
113118
// so for the purpose of the dependency digest its sufficient to just

pkg/sql/opt_catalog.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -603,15 +603,17 @@ func (oc *optCatalog) LeaseByStableID(ctx context.Context, stableID cat.StableID
603603

604604
// GetDependencyDigest is part of the cat.Catalog interface.
605605
func (oc *optCatalog) GetDependencyDigest() cat.DependencyDigest {
606-
// The stats cache may not be setup in some tests like
606+
// The stats and hints caches may not be setup in some tests like
607607
// TestPortalsDestroyedOnTxnFinish. In which case always
608608
// return the empty digest.
609-
if oc.planner.ExecCfg().TableStatsCache == nil {
609+
if oc.planner.ExecCfg().TableStatsCache == nil ||
610+
oc.planner.ExecCfg().StatementHintsCache == nil {
610611
return cat.DependencyDigest{}
611612
}
612613
return cat.DependencyDigest{
613614
LeaseGeneration: oc.planner.Descriptors().GetLeaseGeneration(),
614615
StatsGeneration: oc.planner.execCfg.TableStatsCache.GetGeneration(),
616+
HintsGeneration: oc.planner.execCfg.StatementHintsCache.GetGeneration(),
615617
SystemConfig: oc.planner.execCfg.SystemConfig.GetSystemConfig(),
616618
CurrentDatabase: oc.planner.CurrentDatabase(),
617619
SearchPath: oc.planner.SessionData().SearchPath,

pkg/sql/tests/dependency_digest_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
// TestDependencyDigestOptimization validates that dependency digest information
2323
// is properly invalidated in the face of modifications for prepared queries.
24+
// TODO(drewk,michae2): Once statement hints are hooked up, add a case for them.
2425
func TestDependencyDigestOptimization(t *testing.T) {
2526
defer leaktest.AfterTest(t)()
2627
defer log.Scope(t).Close(t)

0 commit comments

Comments
 (0)